<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ deployment - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ deployment - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 18 May 2026 16:20:51 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/deployment/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Why Your “Simple Deploy” Turned Into a Week of Infrastructure Work ]]>
                </title>
                <description>
                    <![CDATA[ If you're running production workloads, this guide is for you. It's not about side projects, early-stage experiments, or a single-service app with low traffic. This is for teams shipping real systems. ]]>
                </description>
                <link>https://www.freecodecamp.org/news/why-your-simple-deploy-turned-into-a-week-of-infrastructure-work/</link>
                <guid isPermaLink="false">6a022071fca21b0d4b57374f</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ infrastructure ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Mon, 11 May 2026 18:31:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/14f4724b-fb2b-4454-a3dd-3d250e126f50.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you're running production workloads, this guide is for you.</p>
<p>It's not about side projects, early-stage experiments, or a single-service app with low traffic.</p>
<p>This is for teams shipping real systems. Systems with users, uptime expectations, and release pressure.</p>
<p>Because at that stage, your deploy process is no longer a convenience. It's part of your product.</p>
<p>And right now, for most teams, it's the weakest part.</p>
<p>In this article, we'll look at why deployment complexity keeps growing as systems scale, how modern tooling unintentionally pushes teams into platform engineering work, and why many production teams are rethinking the infrastructure they manage themselves.</p>
<p>We'll also look at where Platform as a Service (PaaS) fits into this shift, what trade-offs it introduces, and when adopting one actually makes sense.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-the-promise-you-were-sold">The Promise You Were Sold</a></p>
</li>
<li><p><a href="#heading-the-hidden-contract-you-are-already-operating-under">The Hidden Contract You Are Already Operating Under</a></p>
</li>
<li><p><a href="#heading-you-are-already-acting-like-a-platform-team">You Are Already Acting Like a Platform Team</a></p>
</li>
<li><p><a href="#heading-the-cost-is-not-complexity-it-is-time">The Cost Is Not Complexity. It Is Time</a></p>
</li>
<li><p><a href="#heading-why-it-works-on-my-machine-still-exists">Why “It Works on My Machine” Still Exists</a></p>
</li>
<li><p><a href="#heading-fragmentation-is-the-root-problem">Fragmentation Is the Root Problem</a></p>
</li>
<li><p><a href="#heading-this-model-breaks-as-you-scale">This Model Breaks as You Scale</a></p>
</li>
<li><p><a href="#heading-the-shift-toward-platforms">The Shift Toward Platforms</a></p>
</li>
<li><p><a href="#heading-what-you-stop-paying-for">What You Stop Paying For</a></p>
</li>
<li><p><a href="#heading-from-infrastructure-work-back-to-product-work">From Infrastructure Work Back to Product Work</a></p>
</li>
<li><p><a href="#heading-collapsing-the-stack">Collapsing the Stack</a></p>
</li>
<li><p><a href="#heading-the-trade-off-you-are-actually-making">The Trade-Off You Are Actually Making</a></p>
</li>
<li><p><a href="#heading-when-this-becomes-urgent">When This Becomes Urgent</a></p>
</li>
<li><p><a href="#heading-what-a-simple-deploy-actually-means">What a “Simple Deploy” Actually Means</a></p>
</li>
<li><p><a href="#heading-closing-thought">Closing Thought</a></p>
</li>
</ul>
<h2 id="heading-the-promise-you-were-sold">The Promise You Were Sold</h2>
<p>Every modern stack makes the same promise: Shipping is easy. Deploying is automated. Infrastructure is abstracted away. Push your code. Watch it go live.</p>
<p>That promise works , until it doesn’t.</p>
<p>And when it breaks, it doesn't fail gracefully. It expands.</p>
<p>A “simple deploy” turns into a multi-day investigation across systems you never intended to own.</p>
<p>Not because your team is careless. Because the model itself assumes you'll take on more responsibility than it admits.</p>
<h2 id="heading-the-hidden-contract-you-are-already-operating-under">The Hidden Contract You Are Already Operating Under</h2>
<p>When you deploy today, you're not just shipping code. You're agreeing to run a <a href="https://www.splunk.com/en_us/blog/learn/distributed-systems.html">distributed system</a> of tools.</p>
<p>You own the build pipeline, the container lifecycle, the runtime configuration, the network rules, the secrets layer, the scaling logic, and the observability stack.</p>
<p>Each of these is presented as a separate concern. In reality, they're tightly coupled.</p>
<p>And you're the only layer holding them together. That's the hidden contract.</p>
<h2 id="heading-you-are-already-acting-like-a-platform-team">You Are Already Acting Like a Platform Team</h2>
<p>If your deploy process involves CI pipelines, container registries, cloud services, environment variables, and monitoring tools, you're not just an application team anymore. You're running a platform.</p>
<p>You're defining how code moves from commit to production. You're deciding how failures are handled. And you're shaping how services communicate.</p>
<p>That's platform engineering work.</p>
<p>The issue isn't that this work exists. The issue is that most teams take it on unintentionally, without the structure, tooling, or dedicated ownership a real platform team would require.</p>
<h2 id="heading-the-cost-is-not-complexity-it-is-time">The Cost Is Not Complexity. It Is Time</h2>
<p>It's easy to describe this problem as “complexity.” But that undersells it.</p>
<p>The real cost shows up in how your team spends its time.</p>
<p>Deploys that should take minutes stretch into hours. Then days. Engineers context-switch from product work into debugging <a href="https://www.youtube.com/watch?v=dhiGWtnk4Rk">CI caches</a>, fixing misconfigured secrets, or tracing network failures across services.</p>
<p>Releases slow down. Not because your team can't build features, but because shipping them becomes unpredictable.</p>
<p>Onboarding gets harder. New engineers don't just learn the codebase. They have to learn your deployment system.</p>
<p>None of this appears on a roadmap. But it directly impacts how fast you can move.</p>
<h2 id="heading-why-it-works-on-my-machine-still-exists">Why “It Works on My Machine” Still Exists</h2>
<p>We were supposed to have solved this: Containers. Infrastructure as code. Reproducible builds.</p>
<p>Yet the gap between local and production still shows up at the worst possible moment.</p>
<p>Because the problem was never just environment parity. It's system parity.</p>
<p>Your local setup doesn't include the same limits, permissions, network paths, or scaling behavior as production.</p>
<p>Those differences only surface when everything is wired together. Which means they surface during deploys.</p>
<h2 id="heading-fragmentation-is-the-root-problem">Fragmentation Is the Root Problem</h2>
<p>Modern tooling didn't remove infrastructure complexity. It redistributed it.</p>
<p>Instead of managing servers, you manage integrations between services. Instead of a single failure domain, you have many.</p>
<p>A deploy can fail because of a CI issue, a registry timeout, a secret misconfiguration, a networking rule, or a scaling limit.</p>
<p>Each lives in a different system. Each requires different context.</p>
<p>Individually, these tools are well-designed. Collectively, they form a system that's hard to reason about under pressure.</p>
<h2 id="heading-this-model-breaks-as-you-scale">This Model Breaks as You Scale</h2>
<p>This only works while your system is small. But production systems don't stay small.</p>
<p>More services mean more pipelines. More configurations. More failure points.</p>
<p>Over time, the effort required to maintain your deployment system grows faster than the product itself.</p>
<p>That is the inflection point: where engineering time shifts away from building features and toward maintaining the machinery that ships them.</p>
<p>If you're already feeling that shift, it's not temporary. It's structural.</p>
<p>At some point, there's a question that becomes hard to ignore: Why are you still managing this yourself?</p>
<p>Not because you can't. But because it's no longer clear that you should.</p>
<h2 id="heading-the-shift-toward-platforms">The Shift Toward Platforms</h2>
<p>This is where <a href="https://www.freecodecamp.org/news/from-metrics-to-meaning-how-paas-helps-developers-understand-production/">Platform as a Service</a> changes the model. Not by adding more tools, but by taking ownership of the system those tools create.</p>
<p>A PaaS defines a path from code to production. That path is opinionated, constrained, and consistent.</p>
<p>Those constraints aren't limitations. They're what remove entire categories of failure.</p>
<p>Instead of assembling a deployment pipeline, you adopt one.</p>
<h2 id="heading-what-you-stop-paying-for">What You Stop Paying For</h2>
<p>Moving to a PaaS is often framed as convenience. For production teams, it's closer to cost removal.</p>
<p>You stop spending time deciding how builds run, how services are exposed, how scaling is configured, and how logs are collected.</p>
<p>You stop debugging the integration points between those decisions. You trade flexibility for predictability.</p>
<p>And for most teams, predictability is the constraint that actually matters.</p>
<h2 id="heading-from-infrastructure-work-back-to-product-work">From Infrastructure Work Back to Product Work</h2>
<p>The biggest change isn't in your architecture. It's in your allocation of engineering effort.</p>
<p>Time spent debugging deploys shifts back to building features. Time spent maintaining pipelines shifts to improving the product.</p>
<p>Deploys become routine again. Not because they're simpler in theory, but because the system around them is controlled.</p>
<h2 id="heading-collapsing-the-stack">Collapsing the Stack</h2>
<p>The advantage of a PaaS isn't abstraction. It's consolidation.</p>
<p>Build, deploy, runtime, and observability are integrated into a single system.</p>
<p>There are fewer layers to coordinate. Fewer places to look when something fails. And fewer decisions to get wrong.</p>
<p>Platforms like <a href="https://sevalla.com/">Sevalla</a>, Railway, and Render are pushing this further by tightening the loop between code and production, reducing both the number of systems involved and the surface area developers need to understand.</p>
<p>The goal is operational clarity.</p>
<h2 id="heading-the-trade-off-you-are-actually-making">The Trade-Off You Are Actually Making</h2>
<p>The common objection is control. And it's valid. You give up the ability to customize every layer of your infrastructure.</p>
<p>But in practice, most teams aren't using that control to create differentiation. They're using it to keep a fragile system running, and it’s what keeps teams stuck maintaining systems they shouldn’t own.</p>
<p>Every custom configuration adds another failure point. Another dependency. Another thing to maintain under pressure.</p>
<p>The trade-off isn't control versus convenience. It's control versus reliability.</p>
<h3 id="heading-when-this-becomes-urgent">When This Becomes Urgent</h3>
<p>You don't need a major outage to justify a change. The signals show up earlier.</p>
<p>Deploys feel unpredictable. Releases slow down. Engineers spend more time on pipelines than product logic. Onboarding takes longer than it should.</p>
<p>These aren't isolated issues. They are indicators that your current model isn't scaling with your system.</p>
<h2 id="heading-when-managing-infra-still-makes-sense">When Managing Infra Still Makes Sense</h2>
<p>A PaaS may not right for every team.</p>
<p>If your app is still small, deployments are smooth, and your team isn't spending much time on infrastructure, you may not need a PaaS yet.</p>
<p>Some large companies also choose to build and manage their own platforms. For them, infrastructure is an important part of the business, so the extra work is worth it.</p>
<p>The important thing is making that choice on purpose.</p>
<p>Managing infrastructure is not always a bad thing. The real problem starts when app teams slowly take on platform work without enough people, clear ownership, or the right experience to handle it well.</p>
<h3 id="heading-what-a-simple-deploy-actually-means">What a “Simple Deploy” Actually Means</h3>
<p>A simple deploy isn't one that feels easy when everything works. It's one that continues to work as your system grows.</p>
<p>It's predictable. Failures are rare. When they happen, they're easy to diagnose.</p>
<p>And most importantly, it doesn't require your engineers to think about infrastructure to ship code.</p>
<p>That outcome isn't achieved by adding more tools. It's achieved by reducing the system you have to manage.</p>
<h2 id="heading-closing-thought">Closing Thought</h2>
<p>Your deploy didn't turn into a week of infrastructure work because you missed something. It turned into that because you're operating a model that expects you to.</p>
<p>You can continue investing in that model. Or you can adopt one where deploying is a solved problem.</p>
<p>For production teams, that's no longer a philosophical choice. It's an operational one.</p>
<p><em>Join my</em> <a href="https://applyaito.substack.com/"><em><strong>Applied AI newsletter</strong></em></a> <em>to learn how to build and ship real AI systems. Practical projects, production-ready code, and direct Q&amp;A. You can also</em> <a href="https://www.linkedin.com/in/manishmshiva/"><em><strong>connect with me on</strong></em> <em><strong>LinkedIn</strong></em></a><em><strong>.</strong></em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build and Deploy a Fitness Tracker Using Python Django and PythonAnywhere - A Beginner Friendly Guide ]]>
                </title>
                <description>
                    <![CDATA[ If you've learned some Python basics but still feel stuck when it comes to building something real, you're not alone. Many beginners go through tutorials, learn about variables, functions, and loops,  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-and-deploy-a-fitness-tracker-using-python-django-and-pythonanywhere/</link>
                <guid isPermaLink="false">69cfff6ce466e2b762506a84</guid>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Django ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Prabodh Tuladhar ]]>
                </dc:creator>
                <pubDate>Fri, 03 Apr 2026 17:57:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a1ae273b-9f92-4fc2-89aa-1452fc0df895.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've learned some Python basics but still feel stuck when it comes to building something real, you're not alone. Many beginners go through tutorials, learn about variables, functions, and loops, and then hit a wall when they try to create an actual project.</p>
<p>The gap between "I know Python syntax" and "I can build a working web app" can feel enormous. But it does not have to be.</p>
<p>In this tutorial, you'll build a fitness tracker web application from scratch using Django, one of the most popular Python web frameworks. By the end, you'll have a fully functional app running live on the internet – something you can show to friends, add to your portfolio, or keep building on.</p>
<p>Here's what you'll learn:</p>
<ul>
<li><p>How Django projects and apps are structured</p>
</li>
<li><p>How to define database models to store workout data</p>
</li>
<li><p>How to create views that handle user requests</p>
</li>
<li><p>How to build HTML templates that display your data</p>
</li>
<li><p>How to connect URLs to views so users can navigate your app</p>
</li>
<li><p>How to deploy your finished app to PythonAnywhere so anyone can access it</p>
</li>
</ul>
<p>The app itself is straightforward: you can log a workout by entering an activity name, duration, and date. You can then view all your logged workouts on a separate page. It's simple, but it covers the core Django concepts you need to build much bigger things later.</p>
<p>Let's get started.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-are-going-build">What You Are Going Build</a></p>
</li>
<li><p><a href="#heading-step-1-how-to-set-up-your-django-project">Step 1: How to Set Up Your Django Project</a></p>
<ul>
<li><p><a href="#heading-1-1-how-to-create-a-virtual-environment">1. 1 How to create a virtual environment</a></p>
</li>
<li><p><a href="#heading-12-how-to-install-django">1.2 How to install Django</a></p>
</li>
<li><p><a href="#heading-13-how-to-create-the-project">1.3 How to Create the Project</a></p>
</li>
<li><p><a href="#heading-14-how-to-run-the-development-server">1.4 How to run the development server</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-2-how-to-create-a-django-app">Step 2: How to Create a Django App</a></p>
<ul>
<li><p><a href="#heading-21-how-to-generate-the-app">2.1 How to Generate the App</a></p>
</li>
<li><p><a href="#heading-22-how-to-register-the-app">2.2 How to Register the App</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-3-how-to-create-a-workout-model">Step 3: How to create a Workout Model</a></p>
<ul>
<li><a href="#heading-31-how-to-define-the-model">3.1 How to Define the Model</a></li>
</ul>
</li>
<li><p><a href="#heading-step-4-how-to-apply-migrations">Step 4: How to Apply Migrations</a></p>
<ul>
<li><p><a href="#heading-41-how-to-generate-the-migration">4.1 How to Generate the Migration</a></p>
</li>
<li><p><a href="#heading-42-how-to-apply-the-migration">4.2 How to Apply the Migration</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-5-how-to-register-the-model-in-the-admin-panel">Step 5: How to Register the Model in the Admin Panel</a></p>
<ul>
<li><p><a href="#heading-52-how-to-create-a-superuser">5.2 How to Create a Superuser</a></p>
</li>
<li><p><a href="#heading-53-how-to-access-the-admin-panel">5.3 How to Access the Admin Panel</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-6-how-to-create-views-for-the-app">Step 6: How to Create Views for the App</a></p>
<ul>
<li><p><a href="#heading-61-how-to-create-a-form-class">6.1 How to Create a Form Class</a></p>
</li>
<li><p><a href="#heading-62-how-to-write-views">6.2 How to Write Views</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-7-how-to-create-templates">Step 7: How to Create Templates</a></p>
<ul>
<li><p><a href="#heading-71-how-to-set-up-the-template-directory">7.1 How to Set Up the Template Directory</a></p>
</li>
<li><p><a href="#heading-72-how-to-create-the-workout-list-template">7.2 How to Create the Workout List Template</a></p>
</li>
<li><p><a href="#heading-73-how-to-create-add-workout-template">7.3 How to Create Add Workout Template</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-8-how-to-connect-urls">Step 8: How to Connect URLs</a></p>
<ul>
<li><p><a href="#heading-81-how-to-create-app-level-urls">8.1 How to Create App Level URLs</a></p>
</li>
<li><p><a href="#heading-82-how-to-link-app-urls-to-project">8.2 How to Link App URLs to project</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-9-how-to-test-the-application-locally">Step 9: How to Test the Application Locally</a></p>
</li>
<li><p><a href="#heading-step-10-how-to-prepare-for-deployment">Step 10: How to Prepare for Deployment</a></p>
<ul>
<li><a href="#heading-101-how-to-update-settings-for-production">10.1 How to Update Settings for Production</a></li>
</ul>
</li>
<li><p><a href="#heading-step-11-how-to-deploy-your-django-app-on-pythonanywhere">Step 11: How to Deploy Your Django App on PythonAnywhere</a></p>
<ul>
<li><p><a href="#heading-111-how-to-create-a-pythonanywhere-account">11.1 How to Create a PythonAnywhere Account</a></p>
</li>
<li><p><a href="#heading-112-how-to-upload-your-project-files">11.2 How to Upload Your Project Files</a></p>
</li>
<li><p><a href="#heading-113-how-to-set-up-a-virtual-environment-in-pythonanywhere">11.3 How to Set Up a Virtual Environment in PythonAnywhere</a></p>
</li>
<li><p><a href="#heading-114-how-to-run-migrations-and-create-a-superuser-on-pythonanywhere">11.4 How to Run Migrations and Create a SuperUser on PythonAnywhere</a></p>
</li>
<li><p><a href="#heading-114-how-to-configure-the-web-app-in-pythonanywhere">11.4 How to Configure the Web App in Pythonanywhere</a></p>
</li>
<li><p><a href="#heading-115-how-to-set-the-virtual-environment-path">11.5 How to Set the Virtual Environment Path</a></p>
</li>
<li><p><a href="#heading-116-how-to-configure-the-wsgi-file">11.6 How to Configure the WSGI file</a></p>
</li>
<li><p><a href="#heading-117-how-to-set-up-static-files">11.7 How to Set Up Static Files</a></p>
</li>
<li><p><a href="#heading-118-how-to-view-your-live-application">11.8 How to View Your Live Application</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-common-mistakes-and-how-to-fix-them">Common Mistakes and How to Fix Them</a></p>
</li>
<li><p><a href="#heading-how-you-can-improve-this-project">How You Can Improve This Project</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you are comfortable with the following:</p>
<p><strong>Python fundamentals:</strong> You should understand variables, functions, lists, dictionaries, and basic control flow (if/else statements and loops).</p>
<p><strong>Basic command line usage:</strong> You'll be running commands in your terminal throughout this tutorial. You should know how to open a terminal, navigate between folders, and run commands. If you're on Windows, you can use Command Prompt or PowerShell. On macOS or Linux, the default Terminal app works well.</p>
<p><strong>Tools you'll need installed:</strong></p>
<ul>
<li><p><strong>Python 3.8 or higher.</strong> You can check your version by running <code>python --version</code> or <code>python3 --version</code> in your terminal.&nbsp; If you don't have Python installed, download it from <a href="https://www.python.org">python.org</a></p>
</li>
<li><p><strong>pip.</strong> This is Python's package manager. It usually comes bundled with Python. You can verify by running <code>pip --version</code> or pip3 --version. Note the commands <code>python3</code> and <code>pip3</code> tell the terminal that you are explicitly using <strong>Python Version 3</strong></p>
</li>
<li><p><strong>A code editor.</strong> Visual Studio Code is a great free option, but you can use any editor you're comfortable with.</p>
</li>
</ul>
<p>That's everything. You don't need prior Django experience or web development knowledge. This tutorial will walk you through each step.</p>
<h2 id="heading-what-you-are-going-build">What You Are Going Build</h2>
<p>The fitness tracker you will build has two main features:</p>
<ol>
<li><strong>A form to log workouts.</strong> You will enter the name of an activity (like "Running" or "Push-ups"), how long you did it (in minutes), and the date. When you submit the form, Django saves that workout to a database.</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fe6b2a89-fc29-4710-a640-ce2757267e38.png" alt="The image shows a form to log workouts" style="display:block;margin:0 auto" width="1864" height="1544" loading="lazy">

<ol>
<li><strong>A page to view all your workouts.</strong> This page displays every workout you have logged, showing the activity, duration, and date in a clean list.</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/8f6bd09e-497a-4480-83e5-af162028a0a3.png" alt="The image shows a list of logged workouts" style="display:block;margin:0 auto" width="2144" height="1720" loading="lazy">

<p>Here's how data flows through the app at a high level:</p>
<ol>
<li><p>You fill out the workout form in your browser and click submit.</p>
</li>
<li><p>Your browser sends that data to Django.</p>
</li>
<li><p>Django's view function receives the data, validates it, and saves it to the database.</p>
</li>
<li><p>When you visit the workouts page, Django's view function pulls all saved workouts from the database.</p>
</li>
<li><p>Django passes that data to an HTML template, which renders it as a page your browser can display.</p>
</li>
</ol>
<img alt="The image shows the data flow of the fitness tracker app with 5 steps" style="display:block;margin-left:auto" width="600" height="400" loading="lazy">

<p>This request-response cycle is the foundation of how Django works. Once you understand it, you can build almost anything.</p>
<h2 id="heading-step-1-how-to-set-up-your-django-project">Step 1: How to Set Up Your Django Project</h2>
<p>Every Django project starts with a few setup steps. You'll create an isolated Python environment, install Django, and generate the initial project structure.</p>
<h3 id="heading-1-1-how-to-create-a-virtual-environment">1. 1 How to Create a Virtual Environment</h3>
<p>A virtual environment is a self-contained folder that contains its own Python interpreter and installed packages for a specific project. This keeps your project's dependencies separate from other Python projects on your computer. This separation prevents version conflicts and keeps setups consistent.</p>
<p>For example, one project might require an older version of Django, while another needs the latest version, and a virtual environment allows both to work smoothly on the same system.</p>
<p>Without it, global installations can clash, break projects, and make setups hard to reproduce. Over time, the system environment becomes cluttered with unused or incompatible packages making debugging and maintenance more difficult.</p>
<p>Now let's set it up.</p>
<p>Open your terminal, and navigate to where you want your project to live and run the following command</p>
<pre><code class="language-shell">mkdir fitness-tracker
cd fitness-tracker
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/b4715e51-c2e3-4e97-ad7b-b41066aeefd9.png" alt="An image of the terminal showing the commands mkdir (make directory) and cd (change directory) being typed " style="display:block;margin:0 auto" width="2442" height="542" loading="lazy">

<p>The first command creates a new folder called <code>fitness-tracker</code>. The second command moves you into that folder.</p>
<p>You'll create the Python virutal environment here.</p>
<pre><code class="language-shell">python3 -m venv venv
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3d1723ae-d069-48fd-9954-440a191f585f.png" alt="The image shows the command to create the python virtual enviroment." style="display:block;margin:0 auto" width="2374" height="512" loading="lazy">

<p>The above command creates a virtual environment inside a folder called <code>venv</code>. The first <code>venv</code> is the command and the second <code>venv</code> represents the name of the folder. You can name the folder anything though <code>venv</code> is usually preferred.</p>
<p>By using the <code>ls</code> command, you can see that we've created the virtual environment folder.</p>
<p>To activate the virtual environment, we need to use the following command:</p>
<p>On macOS/Linux:</p>
<pre><code class="language-shell">source venv/bin/activate
</code></pre>
<p>On Windows:</p>
<pre><code class="language-shell">venv\Scripts\activate
</code></pre>
<p>You'll know it worked when you see <code>(venv)</code> at the beginning of your terminal prompt. From this point on, any Python packages you install will only exist inside this <strong>virtual environment</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/dabe362d-2f50-4745-a0bc-e57ad3536723.png" alt="The image shows the virtual environment being activated" style="display:block;margin:0 auto" width="2434" height="1066" loading="lazy">

<h3 id="heading-12-how-to-install-django">1.2 How to Install Django</h3>
<p>With your virtual environment activated, install Django using pip:</p>
<pre><code class="language-shell">pip install django
</code></pre>
<p>This downloads and installs the latest stable version of Django. You can verify the installation by running:</p>
<pre><code class="language-shell">python3 -m django --version
</code></pre>
<p>After running both these commands, you should see Django being installed and the version number:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/bda2ed0d-44bf-439b-a9fd-1cf3fcaf35ca.png" alt="The image shows django being installed and the version of django that has been installed" style="display:block;margin:0 auto" width="2818" height="968" loading="lazy">

<h3 id="heading-13-how-to-create-the-project">1.3 How to Create the Project</h3>
<p>We have finished installing Django. Now let's create a Django project. Django provides a command line utility that generates the boilerplate files that you need. Type the following command:</p>
<pre><code class="language-shell">django-admin startproject fitness_project .
</code></pre>
<p>The command creates a folder named <code>fitness-project</code>. Notice the dot at the end of the command. The dot at the end is important. It tells Django to create the project files in your current directory instead of creating an extra nested folder.</p>
<p>Now that we've created our Django project, let's open the project in your favourite text editor and look at folder structure.</p>
<p>You'll notice that the folder already comes with a bunch of files.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/eaffe95e-7078-4c2e-91f4-88a6c3696e88.png" alt="The image show the list of files created by the django-admin startproject command" width="2362" height="1488" loading="lazy">

<h3 id="heading-14-how-to-run-the-development-server">1.4 How to Run the Development Server</h3>
<p>Now let's make sure everything is working. You'll need to run a server for this. Type the following command:</p>
<pre><code class="language-shell">python manage.py runserver
</code></pre>
<p>You can type this command in the terminal with the virtual environment activated or you can use the integrated terminal if you're using VS Code. I'll be using the integrated terminal from this point on.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/53dbba38-9863-4972-a899-1e6ff66fb3f5.png" alt="This is an image of the server running after typing the runserver command" style="display:block;margin:0 auto" width="2870" height="1662" loading="lazy">

<p>Open your browser and go to <a href="http://127.0.0.1:8000/">http://127.0.0.1:8000/</a>. You should see Django's default welcome page with a rocket ship graphic confirming that your project is set up correctly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/25d2e3ce-ce72-44f6-9f5f-78aeaeb88b3e.png" alt="This is an image of Django's default homepage" style="display:block;margin:0 auto" width="2872" height="1788" loading="lazy">

<p>Press <code>Ctrl + C</code> in your terminal to stop the server when you're ready to move on.</p>
<h2 id="heading-step-2-how-to-create-a-django-app">Step 2: How to Create a Django App</h2>
<p>In Django, a project is the overall container for your entire web application, while an app is a smaller, self-contained module inside that project that focuses on a specific piece of functionality.</p>
<p>A useful way to picture this is to think of a house. The project is the whole house. Each app is like a room inside that house. One room might be a kitchen, another a bedroom, each designed with a clear purpose. In the same way, a Django app is built to handle one responsibility, such as authentication, payments, or in this case, workout tracking.</p>
<p>Now, here's the important part: why not just put everything into one big project instead of using apps? You technically could, especially for very small projects. But as your application grows, that approach quickly becomes difficult to manage.</p>
<p>By using apps, you naturally separate concerns. It also makes collaboration smoother, since different people can work on different apps without constantly stepping on each other’s code.</p>
<p>Another major benefit is reusability. Since apps are modular, you can take an app from one project and reuse it in another.</p>
<p>For example, if you build a workout tracking app once, you could plug it into a completely different Django project later without rebuilding it from scratch. Later, you might create a completely different project, say a fitness coaching platform or a health dashboard. Instead of rebuilding the tracking feature from scratch, you can reuse the same app.</p>
<p>For this project, you'll create a single app called <code>tracker</code> that handles everything related to logging and displaying workouts.</p>
<h3 id="heading-21-how-to-generate-the-app">2.1 How to Generate the App</h3>
<p>Make sure you're in the same directory as the <code>manage.py</code> file, then run the following code:</p>
<pre><code class="language-shell">python manage.py startapp tracker
</code></pre>
<p>This create a new folder called tracker with the following following structure:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cb07105a-6e65-49f9-9c7a-d5a64db42b49.png" alt="The image shows the folder strucutre created by after running the startapp command" style="display:block;margin:0 auto" width="2860" height="1250" loading="lazy">

<p>Each file has its own purpose. You'll work with <code>models.py</code>, <code>views.py</code> and <code>admin.py</code> throughout this project.</p>
<h3 id="heading-22-how-to-register-the-app">2.2 How to Register the App</h3>
<p>Django doesn't automatically know about your new app. You need to tell it by adding the app to the <code>INSTALLED_APPS</code> list in <code>settings.py</code> file.</p>
<p>Open <code>fitness_project/settings.py</code> and find the <code>INSTALLED_APPS</code> list. Add the name of the app, that is <code>tracker</code>, to the end of the list:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/eec90a01-5219-449e-97f8-97465e4ac23f.png" alt="eec90a01-5219-449e-97f8-97465e4ac23f" style="display:block;margin:0 auto" width="2868" height="1306" loading="lazy">

<p>You'll notice that a number of apps have already been installed automatically by Django. This is part of Django’s “batteries-included” philosophy, where many common features are ready to use out of the box.</p>
<p>Here is a short summary of what each of the apps does.</p>
<table>
<thead>
<tr>
<th><strong>App Name</strong></th>
<th><strong>Purpose</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>django.contrib.admin</strong></td>
<td>Powers the built-in admin dashboard, letting you manage your data through a web interface.</td>
</tr>
<tr>
<td><strong>django.contrib.auth</strong></td>
<td>Handles users, login systems, permissions, and password management.</td>
</tr>
<tr>
<td><strong>django.contrib.contenttypes</strong></td>
<td>Helps Django track and manage relationships between different models.</td>
</tr>
<tr>
<td><strong>django.contrib.sessions</strong></td>
<td>Stores user session data, so users stay logged in across requests.</td>
</tr>
<tr>
<td><strong>django.contrib.messages</strong></td>
<td>Lets you show temporary notifications like success or error messages.</td>
</tr>
<tr>
<td><strong>django.contrib.staticfiles</strong></td>
<td>Manages static assets such as CSS, JavaScript, and images</td>
</tr>
</tbody></table>
<p>Now Django knows your <code>tracker</code> app exists and will include it when running the project.</p>
<h2 id="heading-step-3-how-to-create-a-workout-model">Step 3: How to Create a Workout Model</h2>
<p>A model in Django is a Python class that defines the structure of your data. Each model maps directly to a table in your database. Each attribute on the model becomes a column in that table.</p>
<p>Think of a model as a blueprint for a spreadsheet. The class name is the name of the spreadsheet, and each field is a column header. Every time you save a new workout, Django creates a new row in that spreadsheet.</p>
<h3 id="heading-31-how-to-define-the-model">3.1 How to Define the Model</h3>
<p>Open <code>tracker/models.py</code> and replace its contents with this code:</p>
<pre><code class="language-python">from django.db import models

class Workout(models.Model):
    activity = models.CharField(max_length=200)
    duration = models.IntegerField(help_text="Duration in minutes")
    date = models.DateField()

    def __str__(self):
        return f"{self.activity} - {self.duration} min on {self.date}"
</code></pre>
<p>Let's discuss what each part does:</p>
<ul>
<li><p><code>activity = models.CharField(max_length=200)</code> creates a text fields that can hold up to 200 characters. This is where you'll store the name of the exercise like "Running" or "Cycling".</p>
</li>
<li><p><code>duration = models.IntegerField(help_text="Duration in minutes")</code> creates a whole number field for storing how many minutes the workout lasted. The <code>help_text</code> parameter adds a hint that will appear in forms and the admin panel.</p>
</li>
<li><p><code>date = models.DateField()</code> creates a date field for recording when the workout happened.</p>
</li>
</ul>
<p>The <code>__str__()</code> method defines how a Workout object appears when printed or displayed in the admin panel. Instead of seeing something unhelpful like "<strong>Workout object (1)</strong>," you will see "<strong>Running - 30 min on 2025-03-15.</strong>"</p>
<h2 id="heading-step-4-how-to-apply-migrations">Step 4: How to Apply Migrations</h2>
<p>You've defined your model, but Django hasn't created the actual database table yet. To do that, you need to run migrations.</p>
<p>Migrations are Django's way of translating your Python model definitions into database instructions. Migrations are done in two steps.</p>
<p>When you change a model – maybe by adding a field, removing a field, or renaming one – you create a new migration that describes that change. You can do this using the <code>makemigrations</code> command.</p>
<p>Then you apply the migration using the <code>migrate</code> command and Django updates the database to match.</p>
<p>This two-step process of first detecting the change and then applying the change gives you a reliable record of every change to your database structure over time.</p>
<h3 id="heading-41-how-to-generate-the-migration">4.1 How to Generate the Migration</h3>
<p>Run the following command in the integrated terminal:</p>
<pre><code class="language-shell">python manage.py makemigrations
</code></pre>
<p>You should see output like this:</p>
<pre><code class="language-shell">Migrations for 'tracker': tracker/migrations/0001_initial.py 
    + Create model Workout
</code></pre>
<p>Django inspected your Workout model and created a migration file that describes how to build the corresponding database table. You can find this file at <code>tracker/migrations/0001_initial.py</code> if you want to look at it, but you don't need to edit it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fa46eed5-6ef3-408a-8c23-f39518b117f4.png" alt="The image shows the file creating after makemigrations command runs" style="display:block;margin:0 auto" width="2880" height="1424" loading="lazy">

<h3 id="heading-42-how-to-apply-the-migration">4.2 How to Apply the Migration</h3>
<p>Now tell Django to execute that migration and actually create the table in the database:</p>
<pre><code class="language-shell">python manage.py migrate
</code></pre>
<p>You'll see several lines of output as Django applies not just your migration, but also the default migrations for Django's built-in apps (authentication, sessions, and so on).</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fcaae5fe-0cc7-4c1f-b4c3-a3b173fd2551.png" alt="The image shows the output after applying migrations" style="display:block;margin:0 auto" width="2864" height="1650" loading="lazy">

<p>When it finishes, your database has a table ready to store workouts.</p>
<p>When the migrate command runs, we can see the exact SQL commands that Django used to build and change the database. Though this isn't required for creating the application, it's always good to know what's happening under hood.</p>
<p>Run this command:</p>
<pre><code class="language-shell">python manage.py sqlmigrate tracker 001
</code></pre>
<p>And you should get this output:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/016b0a33-06d0-47e8-97de-580e79a7d0e3.png" alt="The image shows the command to view sql queries created by django" style="display:block;margin:0 auto" width="2824" height="1628" loading="lazy">

<p>The <code>001</code> you added at the end is the migration number and represents first version of the database schema.</p>
<p>In practice, your workflow usually looks like this: you change your models, run <code>makemigrations</code> to generate the migration files, and then run the <code>migrate</code> command to apply those changes to the database.</p>
<h2 id="heading-step-5-how-to-register-the-model-in-the-admin-panel">Step 5: How to Register the Model in the Admin Panel</h2>
<p>Django comes with a powerful admin interface built in. It gives you a graphical way to view, add, edit, and delete records in your database without writing any extra code. This is incredibly useful during development because you can quickly test your models and see your data.</p>
<p>But by default, it doesn’t know:</p>
<ul>
<li><p>Which models you want to manage</p>
</li>
<li><p>How you want them displayed</p>
</li>
</ul>
<p>So you <em>register</em> models in <code>admin.py</code> to tell Django to include the specific model in the admin interface.</p>
<h3 id="heading-51-how-to-add-model-to-admin">5.1 How to Add Model to Admin</h3>
<p>Open <code>tracker/admin.py</code> and add the following code:</p>
<pre><code class="language-python">from django.contrib import admin
from .models import Workout

admin.site.register(Workout)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/ad017508-993a-4c28-ade1-8e73fa0c6a4a.png" alt="ad017508-993a-4c28-ade1-8e73fa0c6a4a" style="display:block;margin:0 auto" width="2854" height="816" loading="lazy">

<p>This single line tells Django to include the <code>Workout</code> model in the admin interface.</p>
<h3 id="heading-52-how-to-create-a-superuser">5.2 How to Create a Superuser</h3>
<p>To access the admin panel, you need an admin account. Create one by running:</p>
<pre><code class="language-python">python manage.py createsuperuser
</code></pre>
<p>Django will prompt you for a username, email address, and password. Choose something you will remember. The email is optional – you can press Enter to skip it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/4bbc7a15-682e-497d-a4a4-3e2dc4b848ac.png" alt="The image shows the superuser being created by adding username, email and password" style="display:block;margin:0 auto" width="2878" height="1362" loading="lazy">

<h3 id="heading-53-how-to-access-the-admin-panel">5.3 How to Access the Admin Panel</h3>
<p>Start the development server:</p>
<pre><code class="language-python">python manage.py runserver
</code></pre>
<p>Then navigate to <a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a> in your browser. Log in with the credentials you just created.</p>
<p>You should see the Django administration dashboard with a "<strong>Tracker</strong>" section containing your "<strong>Workouts</strong>" model.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/a1e576bf-45f6-40dc-b6b9-69899b2df9d5.png" alt="The image shows the Django admin panel and the Worker model of the Tracker app being added to the admin panel" style="display:block;margin:0 auto" width="2860" height="1406" loading="lazy">

<p>Try clicking "Add" to create a couple of test workouts. This will confirm that your model is working correctly before you build the rest of the app.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/944ea6a4-bc6f-4321-87c0-5c7bcb267e26.png" alt="The image show some workouts (running and cycling) being added to the admin panel" style="display:block;margin:0 auto" width="2876" height="1146" loading="lazy">

<h2 id="heading-step-6-how-to-create-views-for-the-app">Step 6: How to Create Views for the App</h2>
<p>A view in Django is a Python function (or class) that receives a web request and returns a web response. That response could be an HTML page, a redirect, a 404 error, or anything else a browser can handle.</p>
<p>Views are where your application logic lives. They decide what data to fetch, what processing to do, and what to show the user.</p>
<p>For this app, you need two views: one to display the form where users add a workout, and one to display the list of all saved workouts.</p>
<h3 id="heading-61-how-to-create-a-form-class">6.1 How to Create a Form Class</h3>
<p>Before writing the views, you need a Django form that handles the workout input.</p>
<p>Django forms are a built-in way to handle user input like login forms, contact forms, or anything that collects data from a user. Instead of manually writing HTML, validating inputs, and handling errors, Django gives you a structured way to do all of that in one place.</p>
<p>Most user inputs are based on the models you’ve created, and Django can automatically generate forms from those models using <code>ModelForms</code>, which speeds things up significantly.</p>
<p>Let's create a new file called <code>forms.py</code> in the <code>tracker</code> folder and add the following code:</p>
<pre><code class="language-python">from django import forms
from .models import Workout

class WorkoutForm(forms.ModelForm):

    class Meta:
        model = Workout
        fields = ['activity', 'duration', 'date']
        widgets = {
            'date': forms.DateInput(attrs={'type': 'date'}),
        }
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e8bce19c-7184-45b3-9afe-3a5f73cff43b.png" alt="The image shows the file location of forms.py as well the code for forms.py file" style="display:block;margin:0 auto" width="2866" height="1232" loading="lazy">

<p>In the above code, the <code>ModelForm</code> automatically generates form fields based on the <code>Workout</code> model. The <code>widgets</code> dictionary tells Django to render the date field as an HTML date picker instead of a plain text input.</p>
<p>We can actually see the forms being automatically created by Django. For this we need to enter the shell. In the terminal, type the following command:</p>
<pre><code class="language-shell">python manage.py shell
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/951829fb-98e8-48f5-8187-80cc98346e06.png" alt="The image shows the python shell being activated" style="display:block;margin:0 auto" width="2244" height="932" loading="lazy">

<p>Now lets import the <code>WorkoutForm</code> class that we just created.</p>
<p>Type the following code:</p>
<pre><code class="language-shell">from tracker.forms import WorkoutForm
</code></pre>
<p>Notice that we've given the <strong>name of the app</strong> as well when we imported the form.</p>
<p>Then create an object of the <code>WorkoutForm</code> class and print it.</p>
<pre><code class="language-shell">from tracker.forms import WorkoutForm
workoutform = WorkoutForm()
print(workoutform) 
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/b81265a2-7c21-453b-ae57-3cfec97fbaf9.png" alt="The image shows the command to open the python shell where you can execute python statement throught the terminal" style="display:block;margin:0 auto" width="2236" height="652" loading="lazy">

<p>You should get the following output:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/094b0d89-b003-49b6-939d-07eacfb0c745.png" alt="This image shows the html generated from ModelForm" style="display:block;margin:0 auto" width="936" height="632" loading="lazy">

<p>You can see that all the model fields have been renderd as HTML forms and the date field has been created as a date type that is <code>type="date"</code> instead of plain text.</p>
<h3 id="heading-62-how-to-write-views">6.2 How to Write Views</h3>
<p>As we've discussed above, our project has two views: one to add a workout and the other to display all the saved workouts.</p>
<p>First, let's create a view to add a workout. In the <code>tracker/views.py</code> file, type the following code:</p>
<pre><code class="language-python">from django.shortcuts import render, redirect
from .models import Workout

# view to list all workouts
def workout_list(request):
    workouts = Workout.objects.all().order_by('-date')
    return render(request, 'tracker/workout_list.html', {'workouts': workouts})
</code></pre>
<p>Let's walk through this view:</p>
<ul>
<li><p>The <code>workout_list</code> view handles the page that displays all workouts.</p>
</li>
<li><p>It queries the database for every <code>Workout</code> object, orders them by date (most recent first, thanks to the <code>-</code> prefix), and passes that list to a template called <code>workout_list.html</code>.</p>
</li>
<li><p>The <code>render</code> function combines the template with the data and returns the finished HTML page.</p>
</li>
</ul>
<p>To create the logic to add a workout, first add the <code>Workout</code> form import at the end of the import section. Then add the following code after the <code>workout_list</code> view:</p>
<pre><code class="language-python">from django.shortcuts import render, redirect
from .models import Workout
from .forms import WorkoutForm

# view to list all the workouts
def workout_list(request):
    workouts = Workout.objects.all().order_by('-date')
    return render(request, 'tracker/workout_list.html', {'workouts': workouts})

# view to add a workout
def add_workout(request):
    if request.method == 'POST':
        form = WorkoutForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('workout_list')
    else:
        form = WorkoutForm()
    return render(request, 'tracker/add_workout.html', {'form': form})
</code></pre>
<ul>
<li><p>The <code>add_workout</code> view handles both displaying the empty form and processing submitted form data.</p>
</li>
<li><p>When a user first visits the page, the request method is GET, so Django creates a blank form and renders it.</p>
</li>
<li><p>When the user fills out the form and clicks submit, the request method is POST. Django then validates the submitted data, saves it to the database if everything is correct, and redirects the user to the workout list page.</p>
</li>
<li><p>If the data isn't valid, Django re-renders the form with error messages.</p>
</li>
</ul>
<p>Here is the complete views code:</p>
<pre><code class="language-python">from django.shortcuts import render, redirect
from .models import Workout
from .forms import WorkoutForm

# view to list all workouts
def workout_list(request):
    workouts = Workout.objects.all().order_by('-date')
    return render(request, 'tracker/workout_list.html', {'workouts': workouts})

# view to add a workout
def add_workout(request):
    if request.method == 'POST':
        form = WorkoutForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('workout_list')
    else:
        form = WorkoutForm()
    return render(request, 'tracker/add_workout.html', {'form': form})

</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/8a0878c1-f029-49a4-8a7f-308cdb843b62.png" alt="The image shows the complete code for views.py with explanation about the add workout view" style="display:block;margin:0 auto" width="2316" height="1076" loading="lazy">

<h2 id="heading-step-7-how-to-create-templates">Step 7: How to Create Templates</h2>
<p>Templates are HTML files that Django fills in with dynamic data. They're the front end of your application: the part users actually see in their browser.</p>
<h3 id="heading-71-how-to-set-up-the-template-directory">7.1 How to Set Up the Template Directory</h3>
<p>Django looks for templates inside a <code>templates</code> folder within each app. Create the following folder structure inside your <code>tracker</code> app.</p>
<p><code>tracker/templates/tracker</code></p>
<p>The double <code>tracker</code> folder name might look redundant, but it's a Django convention called <strong>template namespacing</strong>. It prevents naming conflicts if you have multiple apps with templates that share the same filename.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cfbe85c3-36dc-413e-918a-aa64d706d2fc.png" alt="The image shows folder structure of the templates folder" style="display:block;margin:0 auto" width="832" height="474" loading="lazy">

<h3 id="heading-72-how-to-create-the-workout-list-template">7.2 How to Create the Workout List Template</h3>
<p>Create a file called <code>tracker/templates/tracker/workout_list.html</code> and add the following code:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;My Workouts&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background-color: #f5f7fa;
            color: #333;
            line-height: 1.6;
            padding: 2rem;
        }

        .container {
            max-width: 700px;
            margin: 0 auto;
        }

        h1 {
            font-size: 1.8rem;
            margin-bottom: 1rem;
            color: #1a1a2e;
        }

        .add-link {
            display: inline-block;
            background-color: #4361ee;
            color: white;
            padding: 0.6rem 1.2rem;
            border-radius: 6px;
            text-decoration: none;
            margin-bottom: 1.5rem;
            font-size: 0.95rem;
        }

        .add-link:hover {
            background-color: #3a56d4;

        }

        .workout-card {
            background: white;
            border-radius: 8px;
            padding: 1rem 1.2rem;
            margin-bottom: 0.8rem;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
            display: flex;
            justify-content: space-between;
            align-items: center;

        }

        .workout-activity {
            font-weight: 600;
            font-size: 1.05rem;

        }

        .workout-details {
            color: #666;
            font-size: 0.9rem;

        }

        .empty-state {
            text-align: center;
            padding: 3rem 1rem;
            color: #888;

        }

    &lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;h1&gt;My Workouts&lt;/h1&gt;
        &lt;a href="{% url 'add_workout' %}" class="add-link"&gt;+ Log a Workout&lt;/a&gt;
        {% if workouts %}
            {% for workout in workouts %}
                &lt;div class="workout-card"&gt;
                    &lt;div&gt;
                        &lt;div class="workout-activity"&gt;{{ workout.activity }}&lt;/div&gt;
                        &lt;div class="workout-details"&gt;{{ workout.duration }} minutes&lt;/div&gt;
                    &lt;/div&gt;
                    &lt;div class="workout-details"&gt;{{ workout.date }}&lt;/div&gt;
                &lt;/div&gt;
            {% endfor %}

        {% else %}
            &lt;div class="empty-state"&gt;
                &lt;p&gt;No workouts logged yet. Start by adding one!&lt;/p&gt;
            &lt;/div&gt;
        {% endif %}
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>There are a few things worth noting here:</p>
<p>If you look closely at the HTML, you'll spot some weird-looking tags wrapped in curly braces ( <code>{% %}</code> and <code>{{ }}</code> ). Think of them as special instructions for Django.</p>
<p>You use the double curly braces (<code>{{ }}</code>) when you want to output or display a piece of data directly on the page.</p>
<p>On the other hand, you use the brace-and-percent-sign combo ( <code>{% %}</code> ) when you need Django to actually perform an action or apply logic, like running a loop or checking a condition.</p>
<p>They allow us to inject dynamic data straight from our Python backend right into our otherwise static HTML.</p>
<p>Lets look at this code snippet for the <code>workout_list.html</code></p>
<pre><code class="language-html">&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;h1&gt;My Workouts&lt;/h1&gt;
        &lt;a href="{% url 'add_workout' %}" class="add-link"&gt;+ Log a Workout&lt;/a&gt;
        {% if workouts %}
            {% for workout in workouts %}
                &lt;div class="workout-card"&gt;
                    &lt;div&gt;
                        &lt;div class="workout-activity"&gt;{{ workout.activity }}&lt;/div&gt;
                        &lt;div class="workout-details"&gt;{{ workout.duration }} minutes&lt;/div&gt;
                    &lt;/div&gt;
                    &lt;div class="workout-details"&gt;{{ workout.date }}&lt;/div&gt;
                &lt;/div&gt;
            {% endfor %}

        {% else %}
            &lt;div class="empty-state"&gt;
                &lt;p&gt;No workouts logged yet. Start by adding one!&lt;/p&gt;
            &lt;/div&gt;
        {% endif %}
    &lt;/div&gt;
&lt;/body&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/864ed3dc-ceda-44f8-ba3d-b061222714c7.png" alt="The image shows the the body section of the workout_list.html with the focus on django template tags" style="display:block;margin:0 auto" width="1988" height="1160" loading="lazy">

<p>There are a few things worth noting here.</p>
<p>Right under the main heading, you'll see this line:<br><code>&lt;a href="{% url 'add_workout' %}"&gt;</code></p>
<p>Instead of hardcoding a web link like <code>href="/add-workout/"</code>, Django uses the <code>{% url %}</code> tag to generate the link dynamically. You pass it the name of the route (in this case, <code>add_workout</code>), and Django automatically figures out the correct URL path.</p>
<p>If you ever change the URL structure in your Python code later, Django updates this link automatically. You never have to hunt through HTML files to fix broken links!</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/2dc56430-d146-4fc9-8ae5-a3fb5a0d7dd1.png" alt="The image highlights the code that generates dynamic url" style="display:block;margin:0 auto" width="1748" height="632" loading="lazy">

<p>The <code>{% if workouts %}</code> block checks whether there are any workouts to display. If the list is empty, it shows a friendly message instead of a blank page.</p>
<p>The <code>{% for workout in workouts %}</code> loop iterates over every workout in the list and renders a card for each one. The double curly braces <code>{{ workout.activity }}</code> insert the value of each field into the HTML</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f75b5c7e-5cd0-457c-901f-e489c26a8175.png" alt="f75b5c7e-5cd0-457c-901f-e489c26a8175" style="display:block;margin:0 auto" width="2012" height="1002" loading="lazy">

<p>Inside the loop, you'll notice tags that look like this:</p>
<ul>
<li><p><code>{{ workout.activity }}</code></p>
</li>
<li><p><code>{{ workout.duration }}</code></p>
</li>
<li><p><code>{{ workout.date }}</code></p>
</li>
</ul>
<p>As Django loops through each workout object, it uses dot notation to peek inside that specific object and grab its details. It grabs the activity type (like "Running"), the duration ("30"), and the date ("March 30"), and prints that exact text directly onto the webpage for the user to see.</p>
<h3 id="heading-73-how-to-create-add-workout-template">7.3 How to Create Add Workout Template</h3>
<p>Create a file called <code>tracker/templates/tracker/add_workout.html</code> and add the following code:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Log a Workout&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;

        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background-color: #f5f7fa;
            color: #333;
            line-height: 1.6;
            padding: 2rem;
        }

        .container {
            max-width: 500px;
            margin: 0 auto;

        }

        h1 {
            font-size: 1.8rem;
            margin-bottom: 1.5rem;
            color: #1a1a2e;
        }

        .form-group {
            margin-bottom: 1.2rem;
        }

        label {
            display: block;
            margin-bottom: 0.3rem;
            font-weight: 600;
            font-size: 0.95rem;

        }

        input[type="text"],
        input[type="number"],
        input[type="date"] {
            width: 100%;
            padding: 0.6rem 0.8rem;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 1rem;
            transition: border-color 0.2s;
        }

        input:focus {
            outline: none;
            border-color: #4361ee;

        }

        .btn {
            background-color: #4361ee;
            color: white;
            padding: 0.7rem 1.5rem;
            border: none;
            border-radius: 6px;
            font-size: 1rem;
            cursor: pointer;
            margin-right: 0.5rem;
        }

        .btn:hover {
            background-color: #3a56d4;
        }

        .back-link {
            color: #4361ee;
            text-decoration: none;
            font-size: 0.95rem;
        }

        .back-link:hover {
            text-decoration: underline;
        }

        .actions {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin-top: 0.5rem;
        }

        .error-list {
            color: #e74c3c;
            font-size: 0.85rem;
            margin-top: 0.3rem;

        }

    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="container"&gt;
       &lt;h1&gt;Log a Workout&lt;/h1&gt;
        &lt;form method="post"&gt;
            {% csrf_token %}
            &lt;div class="form-group"&gt;
                &lt;label for="id_activity"&gt;Activity&lt;/label&gt;
                {{ form.activity }}
                {% if form.activity.errors %}
                    &lt;div class="error-list"&gt;{{ form.activity.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;
            &lt;div class="form-group"&gt;
                &lt;label for="id_duration"&gt;Duration (minutes)&lt;/label&gt;
                {{ form.duration }}
                {% if form.duration.errors %}
                    &lt;div class="error-list"&gt;{{ form.duration.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="form-group"&gt;
                &lt;label for="id_date"&gt;Date&lt;/label&gt;
                {{ form.date }}
                {% if form.date.errors %}
                    &lt;div class="error-list"&gt;{{ form.date.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="actions"&gt;
                &lt;button type="submit" class="btn"&gt;Save Workout&lt;/button&gt;
                &lt;a href="{% url 'workout_list' %}" class="back-link"&gt;Cancel&lt;/a&gt;
            &lt;/div&gt;
        &lt;/form&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>In the previous template, we learned how to display data. Now, we're looking at a form that actually collects data. Handling forms manually in web development can get messy, but Django provides some powerful template tags to do the heavy lifting for us.</p>
<p>Let's look at the Django-specific logic powering this form:</p>
<p>First, right after opening the <code>&lt;form&gt;</code> tag, you'll spot a very important line: <code>{% csrf_token %}</code>. Whenever you submit data to a server using a "POST" method, malicious sites can potentially intercept or forge that request.</p>
<p>By including this <code>{% csrf_token %}</code>, you tell Django to generate a unique, hidden security key for the form. When the user clicks "Save Workout," Django checks this token to guarantee the request is legitimate. <strong>If you forget this tag, Django will simply reject your form!</strong></p>
<pre><code class="language-html">&lt;form method="post"&gt;
            {% csrf_token %}
            &lt;div class="form-group"&gt;
                &lt;label for="id_activity"&gt;Activity&lt;/label&gt;
                {{ form.activity }}
                {% if form.activity.errors %}
                    &lt;div class="error-list"&gt;{{ form.activity.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;
            &lt;div class="form-group"&gt;
                &lt;label for="id_duration"&gt;Duration (minutes)&lt;/label&gt;
                {{ form.duration }}
                {% if form.duration.errors %}
                    &lt;div class="error-list"&gt;{{ form.duration.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="form-group"&gt;
                &lt;label for="id_date"&gt;Date&lt;/label&gt;
                {{ form.date }}
                {% if form.date.errors %}
                    &lt;div class="error-list"&gt;{{ form.date.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="actions"&gt;
                &lt;button type="submit" class="btn"&gt;Save Workout&lt;/button&gt;
                &lt;a href="{% url 'workout_list' %}" class="back-link"&gt;Cancel&lt;/a&gt;
            &lt;/div&gt;
        &lt;/form&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/4ca2a034-119c-4dc7-a14a-cd0a81815c58.png" alt="The image shows a screenshot of the code and highlight the csrf token tag" style="display:block;margin:0 auto" width="1580" height="824" loading="lazy">

<p>Now let's talk about automatically generating the form fields. Instead of manually typing out all the HTML <code>&lt;input&gt;</code> tags for the activity, duration, and date, we let Django do it for us using display tags (<code>{{ }}</code>).</p>
<p>Each <code>{{ form.activity }}</code>, <code>{{ form.duration }}</code>, and <code>{{ form.date }}</code> tag renders the corresponding form input. Django handles the HTML attributes, input types, and validation for you based on the model and form definitions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/c2403685-b78d-4aa7-876d-63ca53481e37.png" alt="This image shows the code that automatically generates HTML forms" style="display:block;margin:0 auto" width="936" height="572" loading="lazy">

<p>The error blocks below each field display validation messages if a user submits invalid data, like entering text in the duration field instead of a number. Users make mistakes. They might leave a required field blank or type text into a number field. Fortunately, Django validates the data for you and sends back errors if something goes wrong.</p>
<p>Underneath each input field, we use a logic block that looks like this:<br><code>{% if form.activity.errors %}</code></p>
<p>This code checks a simple condition: Did the user mess up this specific field? If Django found an error with the "activity" input, the code drops into the if block and uses<code>{{ form.activity.errors }}</code> block to print the exact error message (like "<strong>This field is required</strong>") right below the input box.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/97a61f9d-faf6-4ddd-ab17-d53da88e0d07.png" alt="This image displays the error blocks" style="display:block;margin:0 auto" width="1908" height="1116" loading="lazy">

<p>You may notice that both templates include inline CSS rather than a separate stylesheet. For a small project like this, inline styles keep things simple and self-contained. In a larger project, you would use Django's static files system to manage CSS separately.</p>
<h2 id="heading-step-8-how-to-connect-urls">Step 8: How to Connect URLs</h2>
<p>You have views and templates, but Django doesn't know when to use them yet. You need to map URLs to views so that visiting a specific address in the browser triggers the right view function.</p>
<h3 id="heading-81-how-to-create-app-level-urls">8.1 How to Create App Level URLs</h3>
<p>Create a new file called <code>tracker/urls.py</code> and add the following code:</p>
<pre><code class="language-python">from django.urls import path
from . import views

urlpatterns = [ 
    path('', views.workout_list, name='workout_list'), 
    path('add/', views.add_workout, name='add_workout'), 
]
</code></pre>
<p>Each path function takes three arguments.</p>
<p>The first is the route string that represents a URL pattern (an empty string means the root of the app).</p>
<p>The second is the view function to call when that URL is visited.</p>
<p>The third is a name you can use to reference this URL elsewhere in your code, like in the <code>{% url %}</code> template tags you used earlier.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/d9128270-eec3-43af-93de-08780b4a53f5.png" alt="The image contains the description of three arguments of the path function" style="display:block;margin:0 auto" width="2864" height="1056" loading="lazy">

<h3 id="heading-82-how-to-link-app-urls-to-project">8.2 How to Link App URLs to project</h3>
<p>Now that your app-level URLs are set up, the next step is to connect them to the main project so Django knows where to start routing requests. Think of it like linking a smaller map (your app) to a bigger map (your project), so everything works together smoothly.</p>
<p>Open <code>fitness_project.urls.py</code> and update it to include your app's URLs:</p>
<pre><code class="language-python">from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('tracker.urls')),
]
</code></pre>
<p>The <code>include()</code> function tells Django to look at the URL patterns defined in the <code>tracker/urls.py</code> file whenever someone visits your site. The empty string prefix means your tracker app handles requests at the root of the site.</p>
<p>Here's the full picture of how a request flows through the URL system.</p>
<p>When someone visits <a href="http://127.0.0.1:8000/add/">http://127.0.0.1:8000/add/</a>, Django first checks <code>fitness_project/urls.py</code>. It matches the empty prefix and delegates to <code>tracker/urls.py</code>. There, it matches <code>add/</code> and calls the <code>add_workout view</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/4590c4e3-0d32-4865-90db-a6504ee508c1.png" alt="The image shows the how the URL flows through the system" style="display:block;margin:0 auto" width="2764" height="1146" loading="lazy">

<h2 id="heading-step-9-how-to-test-the-application-locally">Step 9: How to Test the Application Locally</h2>
<p>At this point, your app has everything it needs to work. Let's test it.</p>
<p>Start the development server by running the command:</p>
<pre><code class="language-shell">python manage.py runserver
</code></pre>
<p>Open your browser and visit <a href="http://127.0.0.1:8000/">http://127.0.0.1:8000/</a>. You should see the workout list page with the heading "<strong>My Workouts</strong>" and a button that says "<strong>+ Log a Workout</strong>."</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e15dc4cf-09b0-4ba9-95b1-baba32ce929f.png" alt="The image shows the My Workouts image with the button to log a workout" style="display:block;margin:0 auto" width="2186" height="1006" loading="lazy">

<p>Click that button. You should see the workout form with fields for activity, duration, and date.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/d9790d3e-0b6a-4e1b-b3d9-f305bc254cfc.png" alt="The image shows an empty form to log a workout" style="display:block;margin:0 auto" width="1146" height="838" loading="lazy">

<p>Fill in some test data:</p>
<ul>
<li><p>Activity: Skipping</p>
</li>
<li><p>Duration: 25</p>
</li>
<li><p>Date: Pick today's date from the date picker</p>
</li>
</ul>
<p>Click "<strong>Save Workout</strong>" You should be redirected back to the workout list page, and your new workout should appear as a card.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3f1dbf8f-3547-45ec-961d-cff75092ec02.png" alt="The image shows the workout list after adding a new workout" style="display:block;margin:0 auto" width="1656" height="1042" loading="lazy">

<p>Try adding a few more workouts with different activities and dates. Make sure they all show up on the list page in the correct order (most recent first).</p>
<p>This is also a good time to experiment. Try submitting the form with missing fields and see how Django handles validation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/542e32cf-abf5-4f11-9d4e-0733697c591b.png" alt="The image shows an incomplete form being submitted and a correspoding error message" style="display:block;margin:0 auto" width="1284" height="1012" loading="lazy">

<p>Try accessing the admin panel at <a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a> to see your workouts there as well.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cdfbcbf3-f7b6-4b97-92ee-dcc27e2ce60c.png" alt="This image shows the added workouts in Django admin" style="display:block;margin:0 auto" width="2876" height="848" loading="lazy">

<p>If everything works as expected, you're ready to put your app on the internet.</p>
<h2 id="heading-step-10-how-to-prepare-for-deployment">Step 10: How to Prepare for Deployment</h2>
<p>Running your app on localhost is great for development, but nobody else can see it. Deployment means putting your app on a server that's accessible from anywhere on the internet.</p>
<p>Before you deploy, you'll need to make a few changes to your project's settings.</p>
<h3 id="heading-101-how-to-update-settings-for-production">10.1 How to Update Settings for Production</h3>
<p>Open <code>fitness_project/settings.py</code> and make the following changes.</p>
<p>First, set <code>DEBUG</code> to <code>False</code>.</p>
<p>During development, <code>DEBUG = True</code> shows detailed error pages that help you fix problems. In production, these error pages would expose sensitive information about your code and server to anyone who triggers an error.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/6b8b491e-d0ae-4a93-80bf-92134e46ff22.png" alt="The image shows the DEBUG being set to False in the settings.py file" style="display:block;margin:0 auto" width="2866" height="1348" loading="lazy">

<p>Next, update <code>ALLOWED_HOSTS</code> to include <strong>PythonAnywhere's</strong> <strong>domain</strong>.</p>
<p>This setting tells Django which domain names are allowed to serve your app. Replace yourusername with the actual PythonAnywhere username you will create in the next step.</p>
<pre><code class="language-python">ALLOWED_HOSTS = ['yourusername.pythonanywhere.com']
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/050746f6-d076-41de-8810-08bf539bfda5.png" alt="The image shows the allowed host list being updated to add the pythonanywhere domain" style="display:block;margin:0 auto" width="1622" height="1068" loading="lazy">

<p>Finally, add a <code>STATIC_ROOT</code> setting so Django knows where to collect your static files (CSS, JavaScript, images) for production:</p>
<pre><code class="language-python">import os
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/712e0514-42f6-4292-ae52-99f49b6f162b.png" alt="The image shows the code to collect static files" style="display:block;margin:0 auto" width="2862" height="1054" loading="lazy">

<p>These are the minimum changes needed for a basic deployment.</p>
<p>💡 For a production app handling real user data, you would also want to set a secure SECRET_KEY, configure a proper database like PostgreSQL, and set up HTTPS. But for a learning project, these changes are enough.</p>
<h2 id="heading-step-11-how-to-deploy-your-django-app-on-pythonanywhere">Step 11: How to Deploy Your Django App on PythonAnywhere</h2>
<p>PythonAnywhere is a hosting platform designed specifically for Python web applications. It offers a free tier that's perfect for beginner projects, and it handles much of the server configuration that would otherwise be complex to set up on your own.</p>
<h3 id="heading-111-how-to-create-a-pythonanywhere-account">11.1 How to Create a PythonAnywhere Account</h3>
<p>Go to <a href="http://pythonanywhere.com">pythonanywhere.com</a> and sign up for a free "Beginner" account. Remember the username you choose, because your app will be available at <a href="http://yourusername.pythonanywhere.com"><strong>yourusername.pythonanywhere.com</strong></a><strong>.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e64e7ca4-7d7d-40a7-a56b-259bb706461f.png" alt="The image shows the homepage of pythonanywhere" style="display:block;margin:0 auto" width="2854" height="1782" loading="lazy">

<p>Now signup to the website. Fill in the username, email and password and click on the free tier or now.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fde55ec6-16f4-4a4f-8927-7b01d3e36a96.png" alt="The image shows the various tiers of python anywhere websites" style="display:block;margin:0 auto" width="2758" height="1418" loading="lazy">

<h3 id="heading-112-how-to-upload-your-project-files">11.2 How to Upload Your Project Files</h3>
<p>After logging in, you have two options for getting your project files onto PythonAnywhere.</p>
<h4 id="heading-option-a-upload-using-git">Option A: Upload using Git</h4>
<p>If your project is in a Git repository, open a Bash console from the PythonAnywhere dashboard by clicking "Consoles" and then "Bash." Then clone your repository:</p>
<p>git clone <a href="https://github.com/yourusername/fitness-tracker.git">https://github.com/yourusername/fitness-tracker.git</a></p>
<p>In this tutorial, we won't be using Git. Instead we'll follow the second option.</p>
<h4 id="heading-option-b-upload-files-manually">Option B: Upload files manually</h4>
<p>First go your project folder in your computer and created a compressed version of the project.</p>
<p>IMPORTANT NOTE: When you create the compressed file, make sure to first create a copy of the project somewhere and remove the venv and pycache folder before you compress it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/169dcd52-50f0-44c0-82d4-1dea1196ac88.png" alt="The image shows the project folder being compressed" style="display:block;margin:0 auto" width="1564" height="422" loading="lazy">

<p>Navigate to your home directory and click on upload file tab and upload the compressed file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/633c7b1f-0088-4d2d-8911-b45e86aa39c2.png" alt="The image shows the compressed file being uploaded to pythonanywhere" style="display:block;margin:0 auto" width="2668" height="1096" loading="lazy">

<p>Now we need to unzip the compressed file. To do this, go to the Consoles tab and click on Bash console.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/7774a88e-362e-4680-a19d-32e0ef09fe04.png" alt="The image shows the Consoles tab and bash option" style="display:block;margin:0 auto" width="2692" height="1198" loading="lazy">

<p>The bash console should open. Then type the following command in the console to unzip the folder:</p>
<pre><code class="language-shell">unzip fitness-tracker.zip
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/1ae044ad-8996-4191-868a-33566c2483d9.png" alt="The image shows the result of the unzip command" style="display:block;margin:0 auto" width="2330" height="1078" loading="lazy">

<h3 id="heading-113-how-to-set-up-a-virtual-environment-in-pythonanywhere">11.3 How to Set Up a Virtual Environment in PythonAnywhere</h3>
<p>Open a Bash console from the PythonAnywhere dashboard. Navigate to your project directory and create a fresh virtual environment:</p>
<pre><code class="language-shell">cd fitness-tracker
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/c8113d5d-68a6-400f-ab9f-7c8903485eda.png" alt="The image shows changing the directory to fitness tracker" style="display:block;margin:0 auto" width="2852" height="468" loading="lazy">

<p>Type the following command to install a virtual environment as we've done before and then activate the virtual environment:</p>
<pre><code class="language-shell">python3 -m venv venv

source venv/bin/activate
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/724767df-181c-4fcf-91e0-77c6dac566b0.png" alt="The image shows the virtual environment being created and activated" style="display:block;margin:0 auto" width="2108" height="624" loading="lazy">

<p>Now install Django as before using <code>pip install django</code> command:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/d6c37d87-7497-4ca1-a90f-6cd66422c2e5.png" alt="The image shows django being installed" style="display:block;margin:0 auto" width="1980" height="952" loading="lazy">

<h3 id="heading-114-how-to-run-migrations-and-create-a-superuser-on-pythonanywhere">11.4 How to Run Migrations and Create a SuperUser on PythonAnywhere</h3>
<p>While you're still in the Bash console with your virtual environment activated, run the migrations to create the database tables on the server:</p>
<pre><code class="language-shell">python manage.py makemigrations

python manage.py migrate

python manage.py createsuperuser
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/0b763118-5a1c-4c3f-87f4-693cc7de0da2.png" alt="The image shows the make migrations and migrate commands running" style="display:block;margin:0 auto" width="2026" height="768" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/77183d3c-b4bc-40b7-b00f-5f03d2aea2ab.png" alt="The image shows the super user being created" style="display:block;margin:0 auto" width="1814" height="506" loading="lazy">

<h3 id="heading-114-how-to-configure-the-web-app-in-pythonanywhere">11.4 How to Configure the Web App in Pythonanywhere</h3>
<p>Go to the "Web" tab on the PythonAnywhere dashboard and click "Add a new web app." Follow the setup wizard:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e1296e93-3e52-4327-b12a-4d4555e80845.png" alt="The image shows the web tab and add a new web app button" style="display:block;margin:0 auto" width="2880" height="846" loading="lazy">

<p>Click "Next" on the domain name step (<em>remember the free tier uses</em> <a href="http://yourusername.pythonanywhere.com"><em>yourusername.pythonanywhere.com</em></a>).</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/24495b99-e1dc-437d-8ce8-47c0c962349f.png" alt="The image shows the web console where you specify the domain name" style="display:block;margin:0 auto" width="2870" height="1302" loading="lazy">

<p>Select "Manual configuration" (not "Django" – the manual option gives you more control).</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/db470bd1-bbd1-4c61-b846-b7147d8f820f.png" alt="The image highlight the manual configuration option which should be selected" style="display:block;margin:0 auto" width="2872" height="1326" loading="lazy">

<p>Then choose the Python version that matches what you installed. In my case it's 3.13, so I'll choose 3.13</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/dc51d44e-531c-4871-a225-e68b2db5dc65.png" alt="The image shows the Python version what is being selected" style="display:block;margin:0 auto" width="2780" height="1384" loading="lazy">

<p>Click on Next button and a WSGI (Web Server Gateway Interface) will be created.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3565512b-9c84-4b0d-82d9-3964cb0ff46b.png" alt="The image shows the final page before the web app is created" style="display:block;margin:0 auto" width="2878" height="1422" loading="lazy">

<p>With this we've created the web app:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/eda9fe63-01f4-48bc-98f1-07696bb798bf.png" alt="The image shows the final creation of the web app" style="display:block;margin:0 auto" width="2874" height="1568" loading="lazy">

<p>After you've set up the web app, you have to do two more things:</p>
<ul>
<li><p>Set the virtual environment path</p>
</li>
<li><p>Configure the WSGI file</p>
</li>
</ul>
<h3 id="heading-115-how-to-set-the-virtual-environment-path">11.5 How to Set the Virtual Environment Path</h3>
<p>On the <strong>Web</strong> tab, scroll down to the "<strong>Virtualenv</strong>" section and enter the path to your virtual enviroment. The path to the file should be like this:</p>
<pre><code class="language-shell">/home/yourusername/fitness-tracker/venv
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/413ed047-ecc1-440f-a931-4a57aa91348b.png" alt="The image shows the added path of virtual environment" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-116-how-to-configure-the-wsgi-file">11.6 How to Configure the WSGI file</h3>
<p>Still on the Web tab, scroll to the code section and click on the WSGI configuration file link:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/934d1542-79d6-4140-8bb2-a4389fc17770.png" alt="The image shows the Code section and the WSGI configuration file path" style="display:block;margin:0 auto" width="2264" height="548" loading="lazy">

<p>Delete all the contents and replace them with the content below and save the file:</p>
<pre><code class="language-python">import os
import sys
path = '/home/prabodhtuladhardev/fitness-tracker' #replace with your username
if path not in sys.path:
    sys.path.append(path)

os.environ['DJANGO_SETTINGS_MODULE'] = 'fitness_project.settings'

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/416adac6-61d7-464d-b91f-b3b35272ef08.png" alt="The image shows the edited wsgi.py file and the highlights the save button" style="display:block;margin:0 auto" width="1812" height="986" loading="lazy">

<h3 id="heading-117-how-to-set-up-static-files">11.7 How to Set Up Static Files</h3>
<p>Still on the "Web" tab, scroll down to the "Static files" section. Add an entry:</p>
<ul>
<li><p>URL: <code>/static/</code></p>
</li>
<li><p>Directory: <code>/home/yourusername/fitness-tracker/staticfiles</code></p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f128dbd2-4acc-4387-89f8-d79de8c1e66e.png" alt="The image shows the static files section of the Web tab" style="display:block;margin:0 auto" width="2488" height="728" loading="lazy">

<p>Then go back to your Bash console and run the following command:</p>
<pre><code class="language-shell">python manage.py collectstatic
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/207f8e67-2679-4ccc-8ebf-7d8aae5d7494.png" alt="The image shows the results of the collect static command" style="display:block;margin:0 auto" width="2448" height="600" loading="lazy">

<p>This copies all static files to the staticfiles directory so PythonAnywhere can serve them directly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/a14b75c4-cc1e-48ad-baf2-cf4f00906b85.png" alt="The image shows the folder named static files that was created" style="display:block;margin:0 auto" width="2720" height="1026" loading="lazy">

<p>Go back to the "Web" tab and click the green "Reload" button at the top. This restarts your app with all the new configuration.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/08fab36e-8c8d-4d1b-8342-6e340fcf45d4.png" alt="The image shows the web tab with the reload button" style="display:block;margin:0 auto" width="2874" height="1046" loading="lazy">

<h3 id="heading-118-how-to-view-your-live-application">11.8 How to View Your Live Application</h3>
<p>Open a new browser tab and visit <a href="https://yourusername.pythonanywhere.com">https://yourusername.pythonanywhere.com</a>. You should see your fitness tracker, live on the internet.</p>
<p>Try adding a workout.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/db67dbb1-3cb9-4b86-bfaa-67427ef5eac0.png" alt="The image shows the workout list view being opened in python anywhere" style="display:block;margin:0 auto" width="2290" height="1278" loading="lazy">

<p>Visit the admin panel at <a href="https://yourusername.pythonanywhere.com/admin/">https://yourusername.pythonanywhere.com/admin/</a>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/bd80d1e8-cf23-4ac2-b6a7-35bfdf61d023.png" alt="The image shows the workout django admin being opened in pythonanywhere" style="display:block;margin:0 auto" width="2128" height="1310" loading="lazy">

<p>Everything should work just as it did on your local machine, but now anyone with the link can access it.</p>
<p>This is a meaningful milestone. You've gone from zero to a deployed Django application. Share the link with a friend or post it in a coding community. Seeing your work live on the internet is one of the most motivating experiences in learning to code.</p>
<h2 id="heading-common-mistakes-and-how-to-fix-them">Common Mistakes and How to Fix Them</h2>
<p>Even when you follow each step carefully, things can go wrong. Here are the most common issues beginners run into and how to solve them.</p>
<p><strong>"ModuleNotFoundError: No module named 'django'"</strong> – This usually means your virtual environment isn't activated. Run <code>source venv/bin/activate</code> (macOS/Linux) or <code>venv\Scripts\activate</code> (Windows) and try again. On PythonAnywhere, make sure the <strong>virtualenv</strong> path in the "<strong>Web</strong>" tab points to the correct location.</p>
<p><strong>"DisallowedHost" error</strong> – You forgot to add your domain to <code>ALLOWED_HOSTS</code> in <code>settings.py</code>, or there's a typo. Double-check that it matches your PythonAnywhere URL exactly.</p>
<p><strong>Static files not loading in production</strong> – Make sure you ran <code>python manage.py collectstatic</code> and that the static file mapping on PythonAnywhere points to the <strong>correct staticfiles</strong> directory. Also verify that <code>STATIC_ROOT</code> is set in <code>settings.py</code>.</p>
<p><strong>"No such table" or migration errors</strong> – You probably forgot to run <code>python manage.py migrate</code> after cloning or uploading your project to PythonAnywhere. Run the <code>migrate</code> command in the Bash console.</p>
<p><strong>Changes not showing up on PythonAnywhere</strong> – After making any code changes, you must click the "<strong>Reload</strong>" button on the "<strong>Web</strong>" tab. PythonAnywhere does not automatically detect file changes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/08fab36e-8c8d-4d1b-8342-6e340fcf45d4.png" alt="The image shows the web tab and the reload buttton" style="display:block;margin:0 auto" width="2874" height="1046" loading="lazy">

<h2 id="heading-how-you-can-improve-this-project">How You Can Improve This Project</h2>
<p>The fitness tracker you built is intentionally simple. That's a feature, not a limitation. A working simple project is the perfect foundation for learning more.</p>
<p>Here are some ideas for expanding it.</p>
<ol>
<li><p><strong>Add user authentication:</strong> Right now, anyone who visits the site sees the same workout data. Django has a built-in authentication system that lets you add registration, login, and logout. Each user could then have their own private list of workouts.</p>
</li>
<li><p><strong>Add the ability to edit and delete workouts.</strong> Currently, once a workout is saved, there's no way to change or remove it from the interface (you can do it through the admin panel, but not the main app). Try creating new views and templates for editing and deleting.</p>
</li>
<li><p><strong>Add workout categories or tags.</strong> Let users categorize their workouts as "Cardio," "Strength," "Flexibility," and so on. This would involve adding a new field to the model or creating a separate Category model with a foreign key relationship.</p>
</li>
<li><p><strong>Add charts and progress tracking.</strong> Use a JavaScript charting library like Chart.js to display workout trends over time. For example, you could show a bar chart of total minutes exercised per week.</p>
</li>
<li><p><strong>Build an API with Django REST Framework.</strong> If you want to learn about building APIs, try installing Django REST Framework (DRF) and creating API endpoints for your workouts. This would let you build a mobile app or a separate front end that communicates with your Django back end.</p>
</li>
</ol>
<p>Each of these improvements will teach you something new about Django while building on the foundation you already have.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've built a fully functional fitness tracker web app with Django and deployed it to the internet. That's no small achievement.</p>
<p>Along the way, you learned how Django projects and apps are structured, how models define the shape of your data, how migrations translate those models into database tables, how views handle the logic of your application, how templates render dynamic HTML, and how URLs tie everything together. You also went through the entire deployment process on PythonAnywhere.</p>
<p>These are the core building blocks of Django development. The patterns you practiced here – defining a model, creating a form, writing a view, building a template, and connecting a URL – are the same patterns you will use in every Django project, no matter how complex.</p>
<p>The best way to solidify what you have learned is to keep building. Try one of the improvements mentioned above, or start a completely new project. A calorie tracker, a habit tracker, an expense tracker, or a personal journal would all use the same Django concepts with slightly different models and views.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Self-Host AFFiNE on Windows with WSL and Docker ]]>
                </title>
                <description>
                    <![CDATA[ Depending on cloud apps means that you don't truly own your notes. If your internet goes down or if the company changes its rules, you could lose access. In this article, you'll learn how to build you ]]>
                </description>
                <link>https://www.freecodecamp.org/news/self-host-affine-windows/</link>
                <guid isPermaLink="false">69b2e3051be92d8f177bf807</guid>
                
                    <category>
                        <![CDATA[ self-hosted ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ WSL ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abdul Talha ]]>
                </dc:creator>
                <pubDate>Thu, 12 Mar 2026 16:00:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/950eee10-aa2c-4071-9c40-abaf759f6d10.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Depending on cloud apps means that you don't truly own your notes. If your internet goes down or if the company changes its rules, you could lose access.</p>
<p>In this article, you'll learn how to build your own private workspace using AFFiNE. You'll use Docker Compose to link three separate pieces of software together:</p>
<ul>
<li><p>The AFFiNE Core application.</p>
</li>
<li><p>A PostgreSQL database to store your notes and pages.</p>
</li>
<li><p>A Redis cache to make the app run fast and smooth.</p>
</li>
</ul>
<p>By the end of this article, you'll have a fully functional web app running on your own computer that works just like the cloud version of Notion.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-affine">What is AFFiNE?</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-step-1-preparing-your-workspace">Step 1: Preparing Your Workspace</a></p>
</li>
<li><p><a href="#heading-step-2-getting-the-official-setup-files">Step 2: Getting the Official Setup Files</a></p>
</li>
<li><p><a href="#heading-step-3-configuring-your-environment-env">Step 3: Configuring Your Environment (.env)</a></p>
</li>
<li><p><a href="#heading-step-4-launching-the-system">Step 4: Launching the System</a></p>
</li>
<li><p><a href="#heading-step-5-accessing-the-admin-panel">Step 5: Accessing the Admin Panel</a></p>
</li>
<li><p><a href="#heading-step-6-configuration-making-it-yours">Step 6: Configuration (Making It Yours)</a></p>
</li>
<li><p><a href="#heading-step-7-connecting-the-desktop-app-optional">Step 7: Connecting the Desktop App (Optional)</a></p>
</li>
<li><p><a href="#heading-step-8-stopping-the-server-and-safe-backups">Step 8: Stopping the Server and Safe Backups</a></p>
</li>
<li><p><a href="#heading-step-9-how-to-upgrade-later">Step 9: How to Upgrade Later</a></p>
</li>
<li><p><a href="#heading-common-installation-errors-and-troubleshooting">Common Installation Errors and Troubleshooting</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-affine">What is AFFiNE?</h2>
<p>AFFiNE is an "all-in-one" workspace that combines the powers of writing, drawing, and planning.</p>
<p>While tools like Notion focus on documents and Miro focus on whiteboards, AFFiNE lets you do both in a single space. You can turn your written notes into a visual canvas with one click. This makes it perfect for brainstorming, tracking tasks, and managing your personal knowledge.</p>
<h3 id="heading-the-power-of-self-hosting">The Power of Self-Hosting</h3>
<p>While AFFiNE offers a cloud version, hosting it yourself gives you three major benefits:</p>
<ul>
<li><p><strong>Total data ownership:</strong> Your notes never leave your machine. You own the database.</p>
</li>
<li><p><strong>Privacy in the AI age:</strong> No big tech company can scan your private ideas or use them for AI training.</p>
</li>
<li><p><strong>Real DevOps skills:</strong> Learning how to manage Docker inside WSL is a high-value skill for any modern developer.</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this article, make sure you have these tools ready on your machine:</p>
<ul>
<li><p><strong>WSL 2 Installation:</strong> You must have WSL installed if you are using Windows (I am using Ubuntu for this guide).</p>
</li>
<li><p><strong>Docker and Docker Compose:</strong> These must be installed and running on your machine.</p>
</li>
<li><p><strong>Linux Terminal Commands:</strong> You should be familiar with basic commands like <code>mkdir</code>, <code>cd</code>, and <code>wget</code>.</p>
</li>
</ul>
<h2 id="heading-step-1-preparing-your-workspace">Step 1: Preparing Your Workspace</h2>
<p>To start, create a folder for your AFFiNE files. This keeps your data in one organised place.</p>
<p>Then open your WSL terminal and run these commands:</p>
<pre><code class="language-shell">mkdir affine
cd affine
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/021e4aef-ede1-4bec-b96e-2acaea9d8f40.png" alt="A terminal Showing the commands mkdir and cd" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-step-2-getting-the-official-setup-files">Step 2: Getting the Official Setup Files</h2>
<p>You will download the official configuration files directly from the AFFiNE. In your WSL terminal, run these two commands:</p>
<ol>
<li>Download the Docker Compose file:</li>
</ol>
<pre><code class="language-shell">wget -O docker-compose.yml https://github.com/toeverything/affine/releases/latest/download/docker-compose.yml
</code></pre>
<ol>
<li>Download the Environment template:</li>
</ol>
<pre><code class="language-shell">wget -O .env https://github.com/toeverything/affine/releases/latest/download/default.env.example
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/5b366a5f-b426-4e70-95c0-b469f40d6af5.png" alt="A terminal Showing the commands to download affine" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-step-3-configuring-your-environment-env">Step 3: Configuring Your Environment (.env)</h2>
<p>The <code>.env</code> file is like a hidden settings sheet. It keeps your passwords and setup details private.</p>
<p>To edit this file, you can use Nano, which is a simple text editor built into your Linux terminal. Follow these steps to update your settings:</p>
<ol>
<li><p><strong>Open the file with Nano:</strong></p>
<pre><code class="language-shell">nano .env
</code></pre>
</li>
<li><p><strong>Update the settings:</strong> Use your arrow keys to move around the file. Update these specific lines to match the locations below. This keeps your data safely inside your new <code>affine</code> folder:</p>
<pre><code class="language-plaintext">DB_DATA_LOCATION=./postgres
UPLOAD_LOCATION=./storage
CONFIG_LOCATION=./config

DB_USERNAME=affine
DB_PASSWORD=
DB_DATABASE=affine
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/d0f4a358-e221-45d3-94df-d97b606b4afc.png" alt="A terminal to change the values in env file" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><strong>Save and Exit:</strong> Press Ctrl + O to save.</p>
<ul>
<li><p>Press <strong>Enter</strong> to confirm the filename.</p>
</li>
<li><p>Press <strong>Ctrl + X</strong> to exit the editor.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-step-4-launching-the-system">Step 4: Launching the System</h2>
<p>Run this Docker command to build your workspace:</p>
<pre><code class="language-shell">docker compose up -d
</code></pre>
<p>Docker will download the AFFiNE app and a Postgres database. The <code>-d</code> flag means it will run quietly in the background.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/407237bd-f805-4fca-b15c-6bf001f467e7.png" alt="A terminal Showing the commands for docker compose" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-step-5-accessing-the-admin-panel">Step 5: Accessing the Admin Panel</h2>
<p>Once the terminal says "Started," your private server is live!</p>
<p>Open your web browser and go to:</p>
<pre><code class="language-plaintext">http://localhost:3010/
</code></pre>
<p>The first time you visit this page, you must create an admin account. This is the master key to your server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/780fafda-0afd-4b67-a2fa-6248b4d5d4f3.png" alt="creating an Admin account" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-step-6-configuration-making-it-yours">Step 6: Configuration (Making It Yours)</h2>
<p>There are two ways to configure your server.</p>
<h3 id="heading-the-easy-way-admin-panel"><strong>The Easy Way: Admin Panel</strong></h3>
<p>In your browser, go to <code>http://localhost:3010/admin/settings</code>. You can change your server name or set up emails here.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/0f8d4e97-7a47-4328-8e91-a36582d47143.png" alt="Overview of the settings page" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-the-developer-way-config-file"><strong>The Developer Way: Config File</strong></h3>
<p>You can also create a <code>config.json</code> file inside your <code>./config</code> folder.</p>
<pre><code class="language-json">{
  "$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
  "server": {
    "name": "My Private Workspace"
  }
}
</code></pre>
<h2 id="heading-step-7-connecting-the-desktop-app-optional">Step 7: Connecting the Desktop App (Optional)</h2>
<p>You don't have to use the browser. You can connect the official AFFiNE desktop app.</p>
<ol>
<li><p>Download the AFFiNE desktop app.</p>
</li>
<li><p>Click the workspace list panel in the top left corner.</p>
</li>
<li><p>Click "Add Server" and enter <code>http://localhost:3010</code>.</p>
</li>
<li><p>Log in with your account.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/2c668ed4-3552-420f-9217-e5f8d09f311c.png" alt="Connecting your local server to Affine Server" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/3a12b7f6-33b9-497e-8684-7fd7a09d8c42.png" alt="Overview of Workspace" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-step-8-stopping-the-server-and-safe-backups">Step 8: Stopping the Server and Safe Backups</h2>
<p>You must turn your server off safely before you back up your notes.</p>
<p>To do that, run this command:</p>
<pre><code class="language-shell">docker compose down
</code></pre>
<p>Once it stops, you can safely copy your entire <code>affine</code> folder to a safe place.</p>
<h2 id="heading-step-9-how-to-upgrade-later">Step 9: How to Upgrade Later</h2>
<p>When AFFiNE releases a new version, run these commands inside your <code>affine</code> folder:</p>
<ol>
<li>Download the newest blueprint:</li>
</ol>
<pre><code class="language-shell">wget -O docker-compose.yml https://github.com/toeverything/affine/releases/latest/download/docker-compose.yml
</code></pre>
<ol>
<li>Pull the new images and restart:</li>
</ol>
<pre><code class="language-shell">docker compose pull
docker compose up -d
</code></pre>
<h2 id="heading-common-installation-errors-and-troubleshooting">Common Installation Errors and Troubleshooting</h2>
<h3 id="heading-1-docker-is-not-running">1. Docker is Not Running</h3>
<ul>
<li><p><strong>The Error:</strong> Terminal says <code>docker: command not found</code>.</p>
</li>
<li><p><strong>The Fix:</strong> Open the Docker Desktop app on Windows and wait for it to start.</p>
</li>
</ul>
<h3 id="heading-2-docker-is-not-connected-to-wsl">2. Docker is Not Connected to WSL</h3>
<ul>
<li><strong>The Fix:</strong> In Docker Desktop, go to <strong>Settings &gt; Resources &gt; WSL Integration</strong> and turn it ON for your distro.</li>
</ul>
<h3 id="heading-3-the-port-is-already-in-use">3. The Port is Already in Use</h3>
<ul>
<li><strong>The Fix:</strong> Open <code>docker-compose.yml</code>. Change <code>"3010:3010"</code> to <code>"4000:3010"</code>. You will now visit <code>localhost:4000</code>.</li>
</ul>
<h3 id="heading-4-permission-denied">4. Permission Denied</h3>
<ul>
<li><strong>The Fix:</strong> If you cannot delete a folder, use the sudo command: <code>sudo rm -rf affine/</code>.</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you've successfully built a self-hosted, private workspace. You practised using WSL, Docker Compose, and Postgres. These are valuable skills for any developer.</p>
<p><strong>Your next steps:</strong></p>
<ol>
<li><p>Create a note in AFFiNE documenting what you learned.</p>
</li>
<li><p>Turn off your server (<code>docker compose down</code>) and copy your folder to a backup drive.</p>
</li>
<li><p>Explore Cloudflare Tunnels if you want to access your server from your phone!</p>
</li>
</ol>
<p>Self-hosting takes a little work, but the privacy is worth it.</p>
<p><strong>Let’s connect!</strong> You can find my latest work on my <a href="https://blog.abdultalha.tech/portfolio"><strong>Technical Writing Portfolio</strong></a> or reach out to me on <a href="https://www.linkedin.com/in/abdul-talha/"><strong>LinkedIn</strong></a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy AI-Generated Code on a PaaS Platform ]]>
                </title>
                <description>
                    <![CDATA[ Vibe coding is about momentum. You open your editor, prompt an AI, stitch pieces together, and suddenly you have something that works. Maybe it’s messy. Maybe the architecture is not perfect. But it’s ]]>
                </description>
                <link>https://www.freecodecamp.org/news/deploy-ai-generated-code-using-paas/</link>
                <guid isPermaLink="false">69aa6fede2698e32064e7d79</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vibe coding ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 06 Mar 2026 06:10:53 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/0b8cb97d-4c82-4e26-862f-a9def20b1612.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Vibe coding is about momentum. You open your editor, prompt an AI, stitch pieces together, and suddenly you have something that works.</p>
<p>Maybe it’s messy. Maybe the architecture is not perfect. But it’s live and working, and that’s the point.</p>
<p>Then comes deployment. This is where the vibe usually dies. Suddenly, you’re reading about containers, load balancers, CI/CD pipelines, infrastructure diagrams, and networking concepts you never asked for. You wanted to ship a thing. Instead, you’re learning accidental DevOps.</p>
<p>The truth is simple. Most vibe-coded apps don’t need complex infrastructure. They just need a clean path from code → live URL.</p>
<p>That’s where a Platform-as-a-Service fits in. It removes the infrastructure ceremony and lets deployment feel like a natural extension of building.</p>
<p>This guide is not about perfect production architecture. It’s about shipping fast without losing momentum. In this article, we will look at how to deploy a simple vibe-coded app using Sevalla. There are other options like Railway, render, and so on, with similar features, and you can <a href="https://www.freecodecamp.org/news/top-heroku-alternatives-for-deployment/">pick one from this list</a>.</p>
<h3 id="heading-what-vibe-deployment-actually-means">What “Vibe Deployment” Actually&nbsp;Means</h3>
<p>Traditional deployment advice assumes you’re building a long-term, heavily engineered system.</p>
<p>Vibe coders operate differently. The goal is speed, feedback, and iteration. A vibe-friendly deployment workflow has a few core characteristics:</p>
<ul>
<li><p><strong>Minimal configuration:</strong> You shouldn’t spend hours setting up environments before seeing your app live.</p>
</li>
<li><p><strong>Fast feedback loops:</strong> Every push should quickly show you the result.</p>
</li>
<li><p><strong>Safe defaults:</strong> You shouldn’t need deep infra knowledge to avoid obvious mistakes.</p>
</li>
</ul>
<p>In other words, deployment shouldn’t be a “phase.” It should be part of the normal development loop. You build. You push. It updates. You keep going.</p>
<h3 id="heading-the-typical-vibe-coded-app">The Typical Vibe-Coded App</h3>
<p>Most vibe-coded projects look similar under the hood. There’s usually a frontend generated or accelerated by AI using React, Next.js, Vue, or something equally modern. The backend might be a small API, sometimes written quickly without strict structure.</p>
<p>Data lives in a managed database. Authentication might be glued together from a few libraries.</p>
<p>The code evolves rapidly. Patterns change weekly. Files get renamed, rewritten, or deleted without ceremony. And that’s fine.</p>
<p>The problem is that traditional deployment workflows assume stability and planning. They expect clean separation between environments, carefully defined build pipelines, and long-term operational thinking.</p>
<p>Vibe-coded apps need the opposite: something that tolerates change and rewards experimentation.</p>
<h3 id="heading-the-paas-mental-model">The PaaS Mental&nbsp;Model</h3>
<p>The biggest shift with a PaaS is how you think about deployment. Instead of asking:</p>
<ul>
<li><p>Which server should I use?</p>
</li>
<li><p>How do I configure networking?</p>
</li>
<li><p>What container setup do I need?</p>
</li>
<li><p>How do I maintain and run my server 24/7?</p>
</li>
</ul>
<p>You think in terms of:</p>
<ul>
<li><p>Connect your repository.</p>
</li>
<li><p>Configure the app once.</p>
</li>
<li><p>Deploy automatically.</p>
</li>
</ul>
<p>A PaaS treats your project as a service that can be built and run. You don’t manage infrastructure, you define the minimum information needed to run your code.</p>
<p>There are only a few concepts you really need to understand:</p>
<ul>
<li><p><strong>Services:</strong> Each deployable unit of your app. A frontend or backend typically becomes a service.</p>
</li>
<li><p><strong>Environment variables:</strong> Secrets and configuration that differ between local and production.</p>
</li>
<li><p><strong>Auto builds:</strong> Every code push triggers a build and deployment.</p>
</li>
</ul>
<p>That’s it. The system handles the rest. The result is important: deployment stops being a separate discipline and becomes just another part of coding.</p>
<h3 id="heading-how-to-ship-your-first-app-on-sevalla">How to Ship Your First App on&nbsp;Sevalla</h3>
<p><a href="https://sevalla.com/">Sevalla</a> is a developer-friendly PaaS provider. It offers application hosting, database, object storage, and static site hosting for your projects.</p>
<p>Let’s walk through what deployment actually looks like in practice. I have already written a few tutorials on both <a href="https://www.freecodecamp.org/news/how-to-build-and-deploy-a-loganalyzer-agent-using-langchain/">Python</a> and <a href="https://www.freecodecamp.org/news/build-and-deploy-an-image-hosting-service-on-sevalla/">Node.js</a> projects, building an app from scratch and deploying it on Sevalla.</p>
<h4 id="heading-step-1-connect-your-repository">Step 1: Connect Your Repository</h4>
<p>The starting point is your Git repository. <a href="https://app.sevalla.com/login">Log in</a> to Sevalla using your GitHub account, or you can connect it after logging in with your email.</p>
<p>You connect your project to Sevalla and select the branch you want to deploy. This creates a direct link between your code and the live app.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/dd0c1f28-0699-45a6-8ab6-316a9a17e780.png" alt="Creating an application" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>You can also enable “Automatic deployments”. Once you create an app, deployment becomes automatic. You push code, and Sevalla takes care of building and publishing.</p>
<p>No manual uploads. No SSH sessions. No server setup.</p>
<h4 id="heading-step-2-configure-the-runtime">Step 2: Configure the&nbsp;Runtime</h4>
<p>Next, you define how your app runs. Most modern frameworks are detected automatically. If you’ve built something common, you usually won’t need to tweak much.</p>
<p>This is where you add environment variables. API keys, database URLs, authentication secrets, and anything that shouldn’t live inside your codebase.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/daa3f518-c9fe-412b-959b-7d46931efad1.png" alt="Adding environment variables" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>A simple rule for vibe coders: If it changes between local and production, make it an environment variable. Once set, you rarely need to touch this again.</p>
<h4 id="heading-step-3-deploy">Step 3:&nbsp;Deploy</h4>
<p>Now you deploy. Sevalla builds the application, installs dependencies, and launches it. After a short wait, you get a live URL.</p>
<p>This is the moment that matters. Your app is no longer a local experiment, it’s something real people can use. And importantly, you didn’t need to make infrastructure decisions to get there.</p>
<h4 id="heading-step-4-iterate-like-a-vibe-coder">Step 4: Iterate Like a Vibe&nbsp;Coder</h4>
<p>Now your workflow shines! You make a change locally. Commit. Push. Sevalla rebuilds and redeploys automatically. Your deployment process becomes invisible, just part of your normal coding rhythm.</p>
<p>This matters more than most people realize. When deployment is effortless, you ship more often. When you ship more often, you learn faster. And fast learning is the real advantage of vibe coding.</p>
<h3 id="heading-things-vibe-coders-usually-break-and-how-paas-helps">Things Vibe Coders Usually Break (and How PaaS&nbsp;Helps)</h3>
<p>Even simple deployment workflows can go wrong. Some patterns show up repeatedly.</p>
<ul>
<li><p><strong>Missing environment variables:</strong> The app works locally but crashes in production. A PaaS surfaces configuration clearly, making it easier to spot.</p>
</li>
<li><p><strong>Localhost assumptions.</strong> Hardcoded URLs or local file paths break once deployed. Using environment configuration fixes this early.</p>
</li>
<li><p><strong>File storage confusion.</strong> Local files disappear between deployments. Treat storage as external from day one.</p>
</li>
<li><p><strong>Ignoring logs.</strong> Many developers only look at logs after panic sets in. Sevalla’s centralized logs make debugging faster when something inevitably fails.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/c1d0d65d-3962-4b18-a8b6-5801a02dd303.png" alt="Logs" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The important point: these aren’t advanced problems. They’re beginner deployment mistakes, and the platform’s defaults help you avoid most of them.</p>
<h3 id="heading-the-minimal-production-checklist">The Minimal Production Checklist</h3>
<p>Before you call something “live,” run through a quick checklist:</p>
<ul>
<li><p>Environment variables are set correctly.</p>
</li>
<li><p>The database is external, not local.</p>
</li>
<li><p>Logs are enabled and readable.</p>
</li>
<li><p>Custom domain is connected if needed.</p>
</li>
<li><p>You know how to roll back to a previous version.</p>
</li>
</ul>
<p>That’s enough for most early-stage projects. You don’t need complex monitoring stacks or multi-region infrastructure to start learning from real users.</p>
<h3 id="heading-why-this-workflow-works-for-vibe-builders">Why This Workflow Works for Vibe&nbsp;Builders</h3>
<p>Indie builders and vibe coders succeed by maintaining velocity. The highest hidden cost in software isn’t infrastructure, it’s context switching.</p>
<p>Every time you stop building to become a part-time DevOps engineer, momentum drops.</p>
<p>A PaaS system’s biggest advantage isn’t technical sophistication. It’s psychological. You stay in the builder mindset. You focus on product decisions instead of infrastructure decisions.</p>
<p>And because deployment feels safe, you ship more frequently. Small releases reduce risk, reduce anxiety, and make experimentation normal. This is exactly the environment where small projects grow into real products.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The best deployment system is one you barely think about. For vibe coders, deployment shouldn’t be a scary milestone or a weekend project. It should feel like pressing save, just another step in the creative loop.</p>
<p>Build something. Push it live. Learn from users. Repeat. That’s the real goal. And when deployment stops being a bottleneck, the vibe stays alive.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Top Heroku Alternatives for Deployment in 2026 ]]>
                </title>
                <description>
                    <![CDATA[ For more than a decade, Heroku defined what “developer-friendly deployment” meant. Push code, forget servers, and focus on shipping features. That promise shaped an entire generation of platform-as-a-service products. In 2026, that landscape is chang... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/top-heroku-alternatives-for-deployment/</link>
                <guid isPermaLink="false">698cfce6afbd03d112127750</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Heroku ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Wed, 11 Feb 2026 22:04:22 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770847448732/3783e969-2001-48bd-8ad9-4fbe46d3b8fd.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>For more than a decade, <a target="_blank" href="https://www.heroku.com/">Heroku</a> defined what “developer-friendly deployment” meant. Push code, forget servers, and focus on shipping features.</p>
<p>That promise shaped an entire generation of platform-as-a-service products. In 2026, that landscape is changing.</p>
<p>Heroku has <a target="_blank" href="https://www.heroku.com/blog/an-update-on-heroku/">clearly stated</a> that it’s moving into a sustaining engineering model. The platform remains stable, secure, and supported, but active innovation is no longer the focus.</p>
<p>For many teams, this is acceptable. For others, especially startups and product teams planning three to five years ahead, it raises an important question: where should new applications live?</p>
<p>In this article, we’ll look at five strong Heroku alternatives that are well-positioned for 2026. Each platform approaches deployment differently, but all aim to preserve what developers loved about Heroku while improving on cost, flexibility, or modern workflows.</p>
<h2 id="heading-what-well-cover">What We’ll Cover</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-teams-are-looking-beyond-heroku">Why Teams Are Looking Beyond Heroku</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-sevalla-the-closest-successor-to-classic-heroku">Sevalla: The Closest Successor to Classic Heroku</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-render-a-broad-platform-for-growing-teams">Render: A Broad Platform for Growing Teams</a></p>
</li>
<li><p><a target="_blank" href="http://Fly.io">Fly.io</a><a class="post-section-overview" href="#heading-flyio-global-first-deployment-for-latency-sensitive-apps">: Global-First Deployment for Latency-Sensitive Apps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-upsun-enterprise-grade-control-without-losing-structure">Upsun: Enterprise-Grade Control Without Losing Structure</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-vercel-the-frontend-native-deployment-platform">Vercel: The Frontend-Native Deployment Platform</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-choosing-the-right-heroku-alternative-in-2026">Choosing the Right Heroku Alternative in 2026</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-teams-are-looking-beyond-heroku">Why Teams Are Looking Beyond Heroku</h2>
<p>Heroku’s shift toward maintenance over expansion signals maturity, not failure. But modern teams expect faster iteration, deeper infrastructure control, and tighter integration with cloud-native tooling.</p>
<p>AI workloads, edge computing, and global latency expectations are also reshaping deployment needs.</p>
<p>As a result, teams may want platforms that feel simple on day one but don’t become limiting as scale and complexity grow.</p>
<p>The alternatives discussed here are not identical replacements. Each represents a different philosophy about how applications should be built and operated in 2026.</p>
<h2 id="heading-sevalla-the-closest-successor-to-classic-heroku">Sevalla: The Closest Successor to Classic Heroku</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770718544630/4e5953ac-1bd7-4c38-9274-bd9228e79d1c.jpeg" alt="Sevalla" class="image--center mx-auto" width="1280" height="720" loading="lazy"></p>
<p><a target="_blank" href="https://sevalla.com/heroku-alternative/">Sevalla</a> has quietly positioned itself as one of the most Heroku-like platforms available today. The core idea is familiar. You deploy applications without managing servers, environments are predictable, and the platform stays out of your way.</p>
<p>What makes Sevalla compelling in 2026 is its balance between simplicity and control. It keeps the developer experience tight while avoiding the opaque pricing and rigid abstractions that frustrated many Heroku users over time. Deployments are fast, logs are easy to access, and scaling feels intuitive rather than magical.</p>
<p>Sevalla is particularly attractive for mid-sized teams as well as enterprises that want a clean path from prototype to production. It supports modern application stacks without forcing you into complex infrastructure decisions too early. For teams migrating directly from Heroku, Sevalla often feels like the least disruptive transition.</p>
<p>The platform’s biggest strength is restraint. It doesn’t try to be everything at once. Instead, it focuses on being a reliable home for long-running services, APIs, and background workers. In 2026, that clarity is refreshing.</p>
<p>Built for the enterprise, Sevalla meets the highest standards of security. They are fully compliant with SOC2, ISO 27017, and ISO 27001:2022, ensuring your data stays protected and your requirements are met.</p>
<h2 id="heading-render-a-broad-platform-for-growing-teams">Render: A Broad Platform for Growing Teams</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770718568758/6682062c-de84-4d42-bbcb-3e4312deb76f.png" alt="Render" class="image--center mx-auto" width="1600" height="900" loading="lazy"></p>
<p><a target="_blank" href="https://render.com/docs/render-vs-heroku-comparison">Render</a> takes a more expansive approach. While it’s often compared to Heroku, Render aims to cover a wider range of use cases, from simple web services to complex microservice architectures.</p>
<p>Render stands out because it blends platform simplicity with infrastructure transparency. You still get managed databases, background jobs, and zero-downtime deploys, but you also gain more visibility into how resources are allocated. This makes it easier to reason about cost and performance as systems grow.</p>
<p>For teams that expect to scale steadily, Render offers a comfortable middle ground. It removes much of the operational burden while allowing deeper configuration when needed. Many engineering teams appreciate that Render feels less restrictive than Heroku without pushing them into full DevOps territory.</p>
<p>In 2026, Render is especially popular with SaaS companies that have outgrown entry-level platforms but are not ready to manage Kubernetes clusters themselves. It supports modern CI/CD workflows and integrates well with common developer tools.</p>
<h2 id="heading-flyio-global-first-deployment-for-latency-sensitive-apps">Fly.io: Global-First Deployment for Latency-Sensitive Apps</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770718605158/a6480909-d1a8-49f4-836f-da212f37700d.jpeg" alt="Fly.io" class="image--center mx-auto" width="1000" height="500" loading="lazy"></p>
<p><a target="_blank" href="https://getdeploying.com/flyio-vs-heroku">Fly.io</a> represents a different philosophy entirely. Instead of abstracting infrastructure away, Fly.io embraces it, but makes it programmable and developer-friendly.</p>
<p>Fly.io allows applications to run close to users by deploying workloads across multiple regions by default. This makes it ideal for applications where latency matters, such as real-time collaboration tools, gaming backends, or global APIs.</p>
<p>Unlike Heroku, Fly.io expects developers to understand a bit more about how their application runs. You interact with virtual machines rather than dynos, and configuration is more explicit. However, this added complexity comes with real power.</p>
<p>In 2026, Fly.io appeals strongly to experienced teams that want performance and control without adopting heavy orchestration systems. It’s not always the easiest option, but it is one of the most flexible. For teams willing to invest in understanding the platform, Fly.io can outperform traditional PaaS solutions in both speed and cost efficiency.</p>
<h2 id="heading-upsun-enterprise-grade-control-without-losing-structure">Upsun: Enterprise-Grade Control Without Losing Structure</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770718618266/aa584c8c-3ac2-4a39-a570-755c21783b2a.png" alt="Upsun" class="image--center mx-auto" width="1200" height="630" loading="lazy"></p>
<p><a target="_blank" href="https://upsun.com/blog/a-heroku-alternative/">Upsun</a>, previously known as Platform.sh, brings a more opinionated, enterprise-oriented model to application deployment. It’s designed for teams that care deeply about environment parity, reproducibility, and long-term maintainability.</p>
<p>Upsun treats infrastructure as part of the application. Environments are versioned alongside code, and deployments are deterministic. This approach reduces surprises and makes complex systems easier to reason about over time.</p>
<p>For organizations with compliance requirements or multi-environment workflows, Upsun offers a level of rigor that Heroku never aimed to provide. At the same time, it abstracts away much of the operational burden that typically comes with such control.</p>
<p>In 2026, Upsun is particularly well-suited for regulated industries, large content platforms, and teams with multiple long-lived environments. It’s less about rapid experimentation and more about predictable, repeatable delivery at scale.</p>
<h2 id="heading-vercel-the-frontend-native-deployment-platform">Vercel: The Frontend-Native Deployment Platform</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770718632237/e6a0725d-151a-4a73-9f61-cf5d1550b043.webp" alt="Vercel" class="image--center mx-auto" width="672" height="428" loading="lazy"></p>
<p><a target="_blank" href="https://vercel.com/">Vercel</a> is often discussed in a different category, but it deserves inclusion in any modern deployment conversation. Vercel is optimized for frontend applications, serverless functions, and edge workloads.</p>
<p>If Heroku excelled at hosting monolithic web apps, Vercel excels at composable, frontend-driven architectures. It integrates deeply with modern frameworks and makes global deployment nearly effortless.</p>
<p>In 2026, many applications are frontend-heavy, with APIs split into smaller services or serverless functions. For these use cases, Vercel offers a developer experience that feels faster and more modern than traditional PaaS platforms.</p>
<p>However, Vercel is not a full replacement for Heroku in every scenario. Long-running background jobs and stateful services often live elsewhere. Still, for teams building modern web products, Vercel frequently becomes the centerpiece of their deployment strategy.</p>
<h2 id="heading-choosing-the-right-heroku-alternative-in-2026">Choosing the Right Heroku Alternative in 2026</h2>
<p>There is no single “best” Heroku replacement. The right choice depends on how your application behaves, how your team works, and how much control you want over infrastructure.</p>
<p>Sevalla is ideal for teams that want familiarity and minimal friction. Render suits growing teams that need flexibility without chaos. Fly.io is powerful for global, performance-sensitive systems. Upsun excels in structured, enterprise environments. Vercel dominates frontend-centric architectures.</p>
<p>The common thread is that deployment in 2026 is no longer one-size-fits-all. Heroku set the standard, but the ecosystem has evolved. Today’s platforms offer sharper trade-offs, clearer philosophies, and better alignment with modern development patterns.</p>
<p>For teams starting new projects, the opportunity is clear. You can choose a platform that matches your future, not just your present.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Heroku is not disappearing, and for many existing workloads, it will continue to run reliably for years. However, its shift toward a sustaining engineering model makes one thing clear: teams building new products in 2026 should think carefully about where they place their long-term bets.</p>
<p>Deployment platforms are no longer just hosting choices. They shape how fast teams move, how systems scale, and how painful future migrations become.</p>
<p>In 2026, the strongest deployment strategy is intentional, not inherited. Heroku showed the industry what was possible. Its successors are now defining what comes next.</p>
<p><em>Hope you enjoyed this article. Learn more about me by</em> <a target="_blank" href="https://manishshivanandhan.com/"><strong><em>visiting my website</em></strong></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Dockerize Your Application and Deploy It ]]>
                </title>
                <description>
                    <![CDATA[ Modern applications rarely live in isolation. They move between laptops, staging servers, and production environments. Each environment has its own quirks, missing libraries, or slightly different configurations. This is where many “works on my machi... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-dockerize-your-application-and-deploy-it/</link>
                <guid isPermaLink="false">69851e61087459735f840552</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ containers ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Thu, 05 Feb 2026 22:49:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770331734345/d53fdc31-231b-4194-96e2-efcec036cfb2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Modern applications rarely live in isolation. They move between laptops, staging servers, and production environments.</p>
<p>Each environment has its own quirks, missing libraries, or slightly different configurations. This is where many “works on my machine” problems begin.</p>
<p><a target="_blank" href="https://www.freecodecamp.org/news/what-is-docker-used-for-a-docker-container-tutorial-for-beginners/">Docker</a> was created to solve this exact issue, and it has become a core skill for anyone building and deploying software today.</p>
<p>In this article, you’ll learn how to Dockerize a <a target="_blank" href="https://www.freecodecamp.org/news/how-to-build-and-deploy-a-loganalyzer-agent-using-langchain/">LogAnalyzer Agent project</a> and prepare it for deployment.</p>
<p>We’ll first understand what Docker is and why it matters. Then we’ll walk through converting this FastAPI-based project into a Dockerized application. Finally, we’ll cover how to build and upload the Docker image so it can be deployed to a cloud platform like Sevalla.</p>
<p>You only need a basic understanding of Python for this project. If you want to learn Docker in detail, go through this <a target="_blank" href="https://www.freecodecamp.org/news/how-docker-containers-work/">detailed tutorial</a>.</p>
<h2 id="heading-what-well-cover">What We’ll Cover</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-docker">What is Docker</a>?</p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-docker-matters">Why Docker Matters</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-project">Understanding the Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-writing-the-dockerfile">Writing the Dockerfile</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-handling-environment-variables-in-docker">Handling Environment Variables in Docker</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-docker-image">Building the Docker Image</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-the-container-locally">Testing the Container Locally</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-preparing-the-image-for-deployment">Preparing the Image for Deployment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-adding-the-docker-image-to-sevalla">Adding the Docker Image to Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-what-is-docker">What is Docker?</h2>
<p><a target="_blank" href="https://www.docker.com/">Docker</a> is a tool that packages your application together with everything it needs to run. This includes the operating system libraries, system dependencies, Python version, and Python packages. The result is called a Docker image. When this image runs, it becomes a container.</p>
<p>A container behaves the same way everywhere. If it runs on your laptop, it will run the same way on a cloud server. This consistency is the main reason Docker is so widely used.</p>
<p>For the LogAnalyzer Agent, this means that FastAPI, LangChain, and all Python dependencies will always be available, regardless of where the app is deployed.</p>
<h2 id="heading-why-docker-matters">Why Docker Matters</h2>
<p>Without Docker, deployment usually involves manually installing dependencies on a server. This process is slow and error prone. A missing system package or a wrong Python version can break the app.</p>
<p>Docker removes this uncertainty. You define the environment once, using a Dockerfile, and reuse it everywhere. This makes onboarding new developers easier, simplifies CI pipelines, and reduces production bugs.</p>
<p>For AI-powered services like the LogAnalyzer Agent, Docker is even more important. These services often rely on specific library versions and environment variables, such as API keys. Docker ensures that these details are controlled and repeatable.</p>
<h2 id="heading-understanding-the-project">Understanding the Project</h2>
<p>Before containerizing the application, it’s important to understand its structure. The LogAnalyzer Agent consists of a FastAPI backend that serves an HTML frontend and exposes an API endpoint for log analysis.</p>
<p>The backend depends on Python packages like FastAPI, LangChain, and the OpenAI client. It also relies on an environment variable for the OpenAI API key.</p>
<p>From Docker’s point of view, this is a typical Python web service. That makes it an ideal candidate for containerization.</p>
<p>At this stage, you should clone the <a target="_blank" href="https://github.com/manishmshiva/loganalyzer">project repository</a> to your local machine. You can run the app using the command <code>python app.py</code></p>
<h2 id="heading-writing-the-dockerfile">Writing the Dockerfile</h2>
<p>The <a target="_blank" href="https://docs.docker.com/reference/dockerfile/">Dockerfile</a> is the recipe that tells Docker how to build your image. It starts with a base image, installs dependencies, copies your code, and defines how the application should start.</p>
<p>For this project, a lightweight Python image is a good choice. The Dockerfile might look like this:</p>
<pre><code class="lang-python">FROM python:<span class="hljs-number">3.11</span>-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE <span class="hljs-number">8000</span>
CMD [<span class="hljs-string">"uvicorn"</span>, <span class="hljs-string">"main:app"</span>, <span class="hljs-string">"--host"</span>, <span class="hljs-string">"0.0.0.0"</span>, <span class="hljs-string">"--port"</span>, <span class="hljs-string">"8000"</span>]
</code></pre>
<p>Each line has a purpose: the base image provides Python and the working directory keeps files organized.</p>
<p>Dependencies are installed before copying the full code to improve build caching. The expose instruction documents the port used by the app. The command starts the FastAPI server.</p>
<p>This file alone turns your project into something Docker understands.</p>
<h2 id="heading-handling-environment-variables-in-docker">Handling Environment Variables in Docker</h2>
<p>The LogAnalyzer Agent relies on an OpenAI API key. This key should never be hardcoded into the image. Instead, Docker allows environment variables to be passed at runtime.</p>
<p>During local testing, you can still use a <code>.env</code> file. When running the container, you can pass the variable using Docker’s environment flags or your deployment platform’s settings.</p>
<p>This separation keeps secrets secure and allows the same image to be used in multiple environments.</p>
<h2 id="heading-building-the-docker-image">Building the Docker Image</h2>
<p>Once the Dockerfile is ready, building the image is straightforward. From the root of the project, you run a Docker build command:</p>
<pre><code class="lang-python">docker build -t loganalyzer:latest .
</code></pre>
<p>Docker reads the Dockerfile, executes each step, and produces an image.</p>
<p>This image contains your FastAPI app, the HTML UI, and all dependencies. At this point, you can run it locally to verify that everything works exactly as before.</p>
<p>Running the container locally is an important validation step. If the app works inside Docker on your machine, it’s very likely to work in production as well.</p>
<h2 id="heading-testing-the-container-locally">Testing the Container Locally</h2>
<p>After building the image, you can start a container and map its port to your local machine. When the container starts, Uvicorn runs inside it, just like it did outside Docker.</p>
<pre><code class="lang-python">docker run -d -p <span class="hljs-number">8000</span>:<span class="hljs-number">8000</span> -e OPENAI_API_KEY=your_api_key_here loganalyzer:latest
</code></pre>
<p>You should be able to open a browser, upload a log file, and receive analysis results. If something fails, the container logs will usually point you to missing files or incorrect paths.</p>
<p>This feedback loop is fast and helps you fix issues before deployment.</p>
<h2 id="heading-preparing-the-image-for-deployment">Preparing the Image for Deployment</h2>
<p>At this stage, the Docker image is ready to be uploaded to a container registry. A registry is a place where Docker images are stored and shared. Your deployment platform will later pull the image from this registry.</p>
<p>We’ll use <a target="_blank" href="https://hub.docker.com/">DockerHub</a> to push our image. Create an account and run <code>docker login</code> command to authenticate it with your terminal.</p>
<p>Now let’s tag and push your image to the repository:</p>
<pre><code class="lang-python">docker tag loganalyzer:latest your-dockerhub-username/loganalyzer:latest
docker push your-dockerhub-username/loganalyzer:latest
</code></pre>
<h2 id="heading-adding-the-docker-image-to-sevalla">Adding the Docker Image to Sevalla</h2>
<p>The final step is to upload the Docker image for deployment.</p>
<p>You can choose any cloud provider, like AWS, DigitalOcean, or others, to run your application. I’ll be using Sevalla for this example.</p>
<p><a target="_blank" href="https://sevalla.com/">Sevalla</a> is a developer-friendly PaaS provider. It offers application hosting, database, object storage, and static site hosting for your projects.</p>
<p>Every platform will charge you for creating a cloud resource. Sevalla comes with a $20 credit for us to use, so we won’t incur any costs for this example.</p>
<p><a target="_blank" href="https://app.sevalla.com/login">Log in</a> to Sevalla and click on Applications -&gt; Create new application:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770296138206/6b3399a2-ed25-498b-80d0-e1ec05fabc35.png" alt="Sevalla Home Page" class="image--center mx-auto" width="1000" height="434" loading="lazy"></p>
<p>You can see the option to link your <a target="_blank" href="https://hub.docker.com/r/manishmshiva/loganalyzer">container repository</a>. Use the default settings. Click “Create application”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770296158854/680b220a-2afc-4521-86c5-0436b8a6c408.png" alt="Create New Application" class="image--center mx-auto" width="1000" height="634" loading="lazy"></p>
<p>Now we have to add our OpenAI API key to the environment variables. Click on the “Environment variables” section once the application is created, and save the <code>OPENAI_API_KEY</code> value as an environment variable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770296194532/b0a77d83-bfea-4774-9b42-6cf1bc7d63dd.png" alt="Add environment variables" class="image--center mx-auto" width="1000" height="428" loading="lazy"></p>
<p>We’re now ready to deploy our application. Click on “Deployments” and click “Deploy now”. It will take 2–3 minutes for the deployment to complete.</p>
<p>Once done, click on “Visit app”. You will see the application served via a URL ending with <code>sevalla.app</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770296212110/f9b75458-cb08-461c-9a2d-e1f785be8161.png" alt="Live application" class="image--center mx-auto" width="1000" height="478" loading="lazy"></p>
<p>Congrats! Your log analyser service is now Dockerized and live.</p>
<p>From this point on, deployment becomes simple. A new version of the app is just a new Docker image. You can push an image to the repository and Sevalla will pull it automatically.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Docker turns your application into a portable, predictable unit. For the LogAnalyzer Agent, this means the AI logic, the FastAPI server, and the frontend all move together as one artifact.</p>
<p>By cloning the project, adding a Dockerfile, and building an image, you convert a local prototype into a deployable service. Uploading that image to Sevalla completes the journey from code to production.</p>
<p>Once you’re comfortable with this workflow, you’ll find that Docker isn’t just a deployment tool. It becomes a core part of how you design, test, and ship applications with confidence.</p>
<p><em>Hope you enjoyed this article. Learn more about me by</em> <a target="_blank" href="https://manishshivanandhan.com/"><strong><em>visiting my website</em></strong></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy a MERN Stack Notes App on AWS ]]>
                </title>
                <description>
                    <![CDATA[ Platforms like Vercel, Netlify, and Render simplify deployment by handling infrastructure for you. In this tutorial, we’ll step one layer deeper and work directly with AWS to understand the building blocks behind these platforms. You'll take a small ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-deploy-mern-stack-notes-app-aws/</link>
                <guid isPermaLink="false">696af32341a3a861f59ed367</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ full stack ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Umair Mirza ]]>
                </dc:creator>
                <pubDate>Sat, 17 Jan 2026 02:25:39 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768616328012/274a3de8-32bb-4b56-9f71-0f0723541c7d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Platforms like Vercel, Netlify, and Render simplify deployment by handling infrastructure for you. In this tutorial, we’ll step one layer deeper and work directly with AWS to understand the building blocks behind these platforms.</p>
<p>You'll take a small React and Express notes app and ship it straight to AWS. We'll use EC2 for the API, RDS Postgres for the database, and S3 (optionally CloudFront) for the frontend. If you're new to AWS, you can turn on the Free Tier first: <a target="_blank" href="https://aws.amazon.com/free">https://aws.amazon.com/free</a>.</p>
<p>If you’ve mostly used one-click deployments before, this guide will help you understand what’s happening behind the scenes. You’ll work directly with the core AWS services involved, focusing only on the pieces that matter so you can see how everything fits together. This will also enable you to have more control over cost, security, and scaling.</p>
<p>If you just want to grab the finished code, it's all in this public repo: <a target="_blank" href="https://github.com/umair-mirza/mern-notes-aws">umair-mirza/mern-notes-aws</a>. You can clone or fork it and follow along without creating a new project from scratch.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-youll-build">What You’ll Build</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-mental-map">Mental Map</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-free-tier-basics">Free Tier Basics</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-environment-variables">Environment Variables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-run-it-locally-first">Step #1 - Run It Locally First</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-push-to-github-so-ec2-can-pull">Step #2 - Push to GitHub (So EC2 Can Pull)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-create-aws-resources-quick-path">Step #3 - Create AWS Resources (Quick Path)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-configure-the-ec2-box">Step #4 - Configure the EC2 Box</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-build-and-upload-the-frontend">Step #5 - Build and Upload the Frontend</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-quick-troubleshooting">Step #6 - Quick Troubleshooting</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-secure-and-save">Step #7 - Secure and Save</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-8-verify-end-to-end">Step #8 - Verify End-to-End</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-next-steps">Next Steps</a></p>
</li>
</ul>
<h2 id="heading-what-youll-build"><strong>What You’ll Build</strong></h2>
<p>Before touching any buttons in AWS, it's helpful to know the exact pieces you're trying to build. At the end of this guide, you'll have a classic three-tier web app: a browser-based frontend, a backend API, and a database, all talking to each other over a network.</p>
<ul>
<li><p>API (Express/Node) on EC2</p>
</li>
<li><p>Postgres on RDS (Free Tier eligible)</p>
</li>
<li><p>React/Vite frontend on S3 (CloudFront optional for CDN/HTTPS)</p>
</li>
<li><p>Health check at <code>/api/health</code> and CRUD at <code>/api/notes</code></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>You don't need to be a DevOps expert to follow along, but you should be comfortable running basic commands in a terminal and editing some config files. If you've ever used <code>npm install</code> before, then you're in the right place.</p>
<ul>
<li><p>AWS account + AWS CLI configured (<code>aws configure</code>) – see <a target="_blank" href="https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html">AWS account setup</a> and <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">AWS CLI install</a>.</p>
</li>
<li><p>Node.js 18+ and npm – get it from <a target="_blank" href="http://nodejs.org">nodejs.org</a><a target="_blank" href="https://nodejs.org/">.</a></p>
</li>
<li><p>Git + GitHub repo – see <a target="_blank" href="https://docs.github.com/en/get-started">GitHub getting started</a>.</p>
</li>
<li><p>(Optional) Route 53 domain for a clean URL – <a target="_blank" href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register.html">Route 53 domains</a>.</p>
</li>
</ul>
<h2 id="heading-mental-map"><strong>Mental Map</strong></h2>
<p>AWS throws a lot of jargon at you (VPCs, security groups, subnets). This section is the story version of what happens when someone opens your app in the browser, without any buzzwords. If you can picture this flow, the later AWS screens will feel less scary.</p>
<ul>
<li><p>Browser loads the built React app from S3 (or CloudFront -&gt; S3)</p>
</li>
<li><p>Browser calls the API on EC2 over HTTP/HTTPS</p>
</li>
<li><p>EC2 talks to RDS Postgres on port 5432 inside your VPC</p>
</li>
<li><p>Security groups: allow 80/443 to EC2; allow 5432 only from the EC2 SG to RDS</p>
</li>
</ul>
<h2 id="heading-free-tier-basics"><strong>Free Tier Basics</strong></h2>
<p>AWS can be cheap if you use the free tier, but it can also surprise you with bills if you accidentally orprovision or leave things running. Here are the main knobs that affect cost for this tutorial and what to watch out for.</p>
<ul>
<li><p>EC2: <code>t2.micro</code> or <code>t3.micro</code> ~750 hours/month</p>
</li>
<li><p>RDS: <code>db.t3.micro</code> Postgres/MySQL with ~20 GB storage</p>
</li>
<li><p>S3/CloudFront: Small sites cost pennies - free tier includes some egress</p>
</li>
<li><p>Save money: Stop EC2 when idle. Delete unused buckets/DBs</p>
</li>
</ul>
<h2 id="heading-environment-variables"><strong>Environment Variables</strong></h2>
<p>Environment variables are just configuration values that live outside your code: ports, database URLs, and allowed origins. They keep secrets (like DB passwords) out of your Git repo and let the same code run in different places (local, staging, production) with different settings.</p>
<ul>
<li><p>Backend: <code>PORT</code>, <code>DATABASE_URL</code> (your RDS endpoint), <code>DATABASE_SSL</code> (<code>true</code> on RDS), <code>CORS_ORIGIN</code></p>
</li>
<li><p>Frontend: <code>VITE_API_URL</code> (API base, for example, <code>https://api.example.com/api</code>)</p>
</li>
</ul>
<h2 id="heading-step-1-run-it-locally-first"><strong>Step #1 - Run It Locally First</strong></h2>
<p>Before touching AWS, you want to prove the app actually works on your own machine. This removes a whole category of "Is it AWS or my code?" debugging later. In this step you just install dependencies and run both backend and frontend in dev mode.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> mern-notes-aws

<span class="hljs-comment"># Backend</span>
<span class="hljs-built_in">cd</span> backend
npm install
cp .env.example .env   <span class="hljs-comment"># set DATABASE_URL to RDS (or local Postgres), DATABASE_SSL=true for RDS</span>
npm run dev            <span class="hljs-comment"># API on http://localhost:4000</span>

<span class="hljs-comment"># Frontend (new terminal)</span>
<span class="hljs-built_in">cd</span> frontend
npm install
cp .env.example .env   <span class="hljs-comment"># keep API URL at http://localhost:4000/api for local dev</span>
npm run dev            <span class="hljs-comment"># SPA on http://localhost:5173</span>
</code></pre>
<p>Open <code>http://localhost:5173</code>, add a note, and check if it persists. <code>/api/health</code> should return <code>{ status: 'ok' }</code>. If something is broken here, pause and fix it before moving on. AWS will only make debugging harder.</p>
<h2 id="heading-step-2-push-to-github-so-ec2-can-pull"><strong>Step #2 - Push to GitHub (So EC2 Can Pull)</strong></h2>
<p>Your EC2 server in AWS needs a place to pull your code from. Using GitHub is the simplest option: you push your code once, then the EC2 instance clones that repo. You can also reuse this repo later with CI/CD if you decide to automate deployments.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> mern-notes-aws
git init
git add .
git commit -m <span class="hljs-string">"feat: mern notes app"</span>
git branch -M main
git remote add origin https://github.com/&lt;you&gt;/mern-notes-aws.git
git push -u origin main
</code></pre>
<p>If you're following along with my example repo instead of creating your own, you can simply fork <a target="_blank" href="https://github.com/umair-mirza/mern-notes-aws">umair-mirza/mern-notes-aws</a> and use that as your remote.</p>
<p>Before pushing, make sure your <code>.env</code> file is <strong>not committed to GitHub</strong>. Add it to your <code>.gitignore</code> so secrets like database passwords never end up in version control:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">".env"</span> &gt;&gt; .gitignore
</code></pre>
<p>If you’ve already created a <code>.env</code> file locally, double-check it doesn’t appear in <code>git status</code> before committing.</p>
<h2 id="heading-step-3-create-aws-resources-quick-path"><strong>Step #3 - Create AWS Resources (Quick Path)</strong></h2>
<h3 id="heading-rds-postgres-free-tier-template"><strong>RDS (Postgres, Free Tier template)</strong></h3>
<p>RDS (Relational Database Service) is AWS's way of running managed databases for you. Instead of installing Postgres manually on a VM, you click a few options and AWS handles backups, patching, and high availability. For this app we only need a small, free tier–eligible Postgres instance.</p>
<p>For more background, you can skim the official <a target="_blank" href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html">Amazon RDS for PostgreSQL docs</a>.</p>
<p>We’ll start by creating the database layer. The settings below are the minimum you need for a small, production-style Postgres setup that stays within the AWS Free Tier while still following basic best practices.</p>
<ul>
<li><p>RDS Create database Postgres Free Tier.</p>
</li>
<li><p>Class <code>db.t3.micro</code>, storage 20 GB gp2/gp3.</p>
</li>
<li><p>Set master user/pass. You'll need them for <code>DATABASE_URL</code>.</p>
</li>
<li><p>Public access: No.</p>
</li>
<li><p>Security group: allow 5432 only from the EC2 security group.</p>
</li>
<li><p>Enable backups and Require SSL. Download the RDS CA if you want strict cert validation.</p>
</li>
</ul>
<h3 id="heading-s3-bucket-for-the-frontend"><strong>S3 Bucket for the Frontend</strong></h3>
<p>S3 is AWS's "infinite hard drive" for files. A React/Vite app builds down to plain HTML, CSS, and JavaScript files, which are perfect to host from S3. Think of S3 as a very simple web server that just serves static files.</p>
<p>If you want to see more options, check the <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html">Hosting a static website on Amazon S3 guide</a>.</p>
<p>Now, we’ll create an S3 bucket to host the React frontend. These options configure the bucket for static website hosting while keeping it simple and inexpensive.</p>
<ul>
<li><p>Create bucket <code>mern-notes-aws-frontend-&lt;suffix&gt;</code>.</p>
</li>
<li><p>For simple hosting, enable static website hosting and allow public reads, or keep private and use CloudFront + OAC.</p>
</li>
<li><p>Turn on versioning if you want rollback safety.</p>
</li>
</ul>
<h3 id="heading-ec2-for-the-api"><strong>EC2 for the API</strong></h3>
<p>EC2 is "a computer in the cloud" that you control. You'll install Node.js on it, pull your code, and run <code>server.js</code> so that your backend API is always on. The security group attached to this instance works like a firewall.</p>
<p>If you've never launched an instance before, the <a target="_blank" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html">Getting started with Amazon EC2</a> guide walks through the console screens you'll see.</p>
<p>Finally, we’ll provision a small EC2 instance to run the Express API. The configuration below focuses on a free tier–eligible setup that’s secure enough for learning and easy to extend later.</p>
<ul>
<li><p>Launch Amazon Linux 2023, size <code>t3.micro</code>.</p>
</li>
<li><p>Inbound SG: 22 (your IP), 80 (world), 443 if you add HTTPS on the instance/ALB.</p>
</li>
<li><p>Attach this SG as the allowed source to RDS.</p>
</li>
</ul>
<h3 id="heading-optional-cloudfront-route-53"><strong>Optional: CloudFront + Route 53</strong></h3>
<p>CloudFront is AWS's CDN (content delivery network), and Route 53 is their DNS service. You don't strictly need them to get your app working, but they make it faster and nicer: your app loads from edge locations close to users and can live behind a friendly domain like <code>app.example.com</code>.</p>
<p>For more details, see <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.html">Getting started with Amazon CloudFront</a> and the <a target="_blank" href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html">Route 53 DNS developer guide</a>.</p>
<ul>
<li><p>Origin: the S3 bucket. Default root <code>index.html</code>. Add OAC if bucket is private.</p>
</li>
<li><p>Request an ACM cert in <code>us-east-1</code>, then create a Route 53 A/AAAA alias to the distribution.</p>
</li>
</ul>
<h2 id="heading-step-4-configure-the-ec2-box"><strong>Step #4 - Configure the EC2 Box</strong></h2>
<p>Once your EC2 instance is running, you treat it like a clean Linux machine. The commands below install the tools your API needs, pull your code from GitHub, configure environment variables, and run the server in a production-safe way.</p>
<p>Install basics:</p>
<pre><code class="lang-bash">sudo dnf update -y
</code></pre>
<p>This command updates all system packages to the latest versions. It's a good first step on any new Linux server.</p>
<pre><code class="lang-bash">sudo dnf install -y git
</code></pre>
<p>Installs Git so the EC2 instance can clone your repository from GitHub.</p>
<pre><code class="lang-bash">curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
</code></pre>
<p>Adds the official NodeSource repository so you can install a modern version of Node.js (v20). Amazon Linux doesn’t ship with recent Node versions by default.</p>
<pre><code class="lang-bash">sudo dnf install -y nodejs
</code></pre>
<p>Installs Node.js and npm, which are required to run your Express API.</p>
<pre><code class="lang-bash">sudo npm install -g pm2
</code></pre>
<p>Installs PM2, a lightweight process manager that keeps your Node app running in the background and restarts it if it crashes or the server reboots.</p>
<p>Pull code and set environment variables:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/&lt;you&gt;/mern-notes-aws.git
<span class="hljs-built_in">cd</span> mern-notes-aws/backend
npm install

cat &lt;&lt;<span class="hljs-string">'EOF'</span> &gt; .env
PORT=80
DATABASE_URL=postgres://&lt;user&gt;:&lt;password&gt;@&lt;rds-endpoint&gt;:5432/&lt;dbname&gt;
DATABASE_SSL=<span class="hljs-literal">true</span>
CORS_ORIGIN=https://&lt;your-frontend-domain&gt;
EOF
</code></pre>
<p>Start the API with PM2:</p>
<pre><code class="lang-bash">pm2 start server.js --name mern-notes-api
pm2 save
pm2 startup systemd -u ec2-user --hp /home/ec2-user
</code></pre>
<p>PM2 is a small process manager that makes sure your Node server keeps running if the machine reboots or the process crashes. Test on the box: <code>curl http://localhost/api/health</code>. From your laptop: <code>http://&lt;ec2-public-dns&gt;/api/health</code> (make sure SG allows 80/443).</p>
<h2 id="heading-step-5-build-and-upload-the-frontend"><strong>Step #5 - Build and Upload the Frontend</strong></h2>
<p>In development, Vite serves your React app from memory, but in production you want a set of static files that any web server (or S3) can host. <code>npm run build</code> creates an optimized <code>dist/</code> folder that you sync to S3 so the browser can load it.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> frontend
setx VITE_API_URL <span class="hljs-string">"https://&lt;ec2-or-api-domain&gt;/api"</span>
npm run build
</code></pre>
<p>This sets an environment variable called <code>VITE_API_URL</code> on your local machine. Vite only exposes environment variables to the frontend if they start with the <code>VITE_</code> prefix.</p>
<p>Upload:</p>
<pre><code class="lang-bash">aws s3 sync dist/ s3://mern-notes-aws-frontend-&lt;suffix&gt;/ --delete
</code></pre>
<p>This uploads your compiled frontend (<code>dist/</code>) to S3 and removes old files that no longer exist locally, ensuring the bucket reflects the current version of the app</p>
<p>Open the S3 website URL or your CloudFront URL.</p>
<h2 id="heading-step-6-quick-troubleshooting"><strong>Step #6 - Quick Troubleshooting</strong></h2>
<p>If something doesn't work the first time, that's normal, especially with networking and AWS permissions. This section gives you a few quick places to look before you start randomly changing settings in the console.</p>
<ul>
<li><p>API 500s: <code>pm2 logs mern-notes-api</code>. This is often a bad <code>DATABASE_URL</code> or SSL flag.</p>
</li>
<li><p>DB connect issues: RDS SG must allow the EC2 SG - use the RDS endpoint.</p>
</li>
<li><p>CORS errors: <code>CORS_ORIGIN</code> must match your frontend origin exactly.</p>
</li>
<li><p>403 from S3: If you’re using static website hosting, allow public reads. With CloudFront, keep bucket private and use OAC.</p>
</li>
<li><p>Blank page: Confirm that you’ve uploaded <code>dist/</code> to the right bucket.</p>
</li>
</ul>
<h2 id="heading-step-7-secure-and-save"><strong>Step #7 - Secure and Save</strong></h2>
<p>Once everything works, you don't want to accidentally expose your database to the internet or burn through free tier hours. These are simple, beginner-friendly hardening steps that make your setup safer and cheaper without turning you into a full-time security engineer.</p>
<ul>
<li><p>Turn off SSH after setup or switch to SSM Session Manager.</p>
</li>
<li><p>Use HTTPS (CloudFront + ACM or ALB + ACM).</p>
</li>
<li><p>Keep RDS private and use SSM port forwarding if needed.</p>
</li>
<li><p>Ship PM2 logs with CloudWatch Agent and add alarms for CPU/status checks.</p>
</li>
<li><p>Snapshot RDS daily and stop EC2 when idle to save hours.</p>
</li>
</ul>
<h2 id="heading-step-8-verify-end-to-end"><strong>Step #8 - Verify End-to-End</strong></h2>
<p>Before you celebrate, run through the app like a real user: open it in the browser, create notes, refresh, and make sure everything behaves as expected. This confirms your frontend, API, and database are all wired together correctly.</p>
<ul>
<li><p>Load the frontend (S3 or CloudFront).</p>
</li>
<li><p>Create and delete notes. They should persist in RDS.</p>
</li>
<li><p>Hit <code>/api/health</code> for a quick liveness check.</p>
</li>
</ul>
<h2 id="heading-next-steps"><strong>Next Steps</strong></h2>
<p>Once you're comfortable with this manual setup, you can start layering on more advanced tools. The ideas are the same: frontend, API and database but you get more automation, safety, and scalability.</p>
<ul>
<li><p>Add Prisma + migrations for stronger schemas.</p>
</li>
<li><p>Add auth (Cognito/Auth0) and per-user notes.</p>
</li>
<li><p>Containerize and run on ECS/Fargate or add an ALB in front of EC2.</p>
</li>
<li><p>Use Terraform/CDK to recreate this stack with one command.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Manage Blue-Green Deployments on AWS ECS with Database Migrations: Complete Implementation Guide ]]>
                </title>
                <description>
                    <![CDATA[ Blue-green deployments are celebrated for enabling zero-downtime releases and instant rollbacks. You deploy your new version (green) alongside the current one (blue), switch traffic over, and if something goes wrong, you switch back. Simple, right? N... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-manage-blue-green-deployments-on-aws-ecs-with-database-migrations/</link>
                <guid isPermaLink="false">69693109596ef11a775126fb</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Blue/Green deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Databases ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Destiny Erhabor ]]>
                </dc:creator>
                <pubDate>Thu, 15 Jan 2026 18:25:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768497873258/be1ce2a3-c95f-488e-913a-a772007a0d2a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Blue-green deployments are celebrated for enabling zero-downtime releases and instant rollbacks. You deploy your new version (green) alongside the current one (blue), switch traffic over, and if something goes wrong, you switch back. Simple, right?</p>
<p>Not quite. While blue-green deployments work beautifully for stateless applications, they become significantly more complex when you introduce databases and stateful services into the equation. The moment your blue and green environments need to share a database, you're facing a fundamental challenge: how do you evolve your schema and data without breaking either version?</p>
<p>In this article, we'll tackle the real-world complexities of implementing blue-green deployments on Amazon ECS when your application depends on shared state. You'll learn practical strategies for handling database migrations, managing sessions, and maintaining data consistency across application versions.</p>
<p>💡 <strong>Complete Working Example</strong>: All code examples in this article are available in the <a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs">bluegreen-deployment-ecs</a> <a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs">repository on GitHub.</a> You can clone it and deploy the entire infrastructure to your AWS account.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-the-problem-with-state-in-blue-green-deployments">The Problem with State in Blue-Green Deployments</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-database-migration-strategies-for-blue-green">Database Migration Strategies for Blue-Green</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-handling-stateful-services-in-ecs">Handling Stateful Services in ECS</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-complete-implementation-end-to-end-example">Complete Implementation: End-to-End Example</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-rollback-strategies">Rollback Strategies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-monitoring-during-deployments">Monitoring During Deployments</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices">Best Practices</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-when-not-to-use-blue-green">When NOT to Use Blue-Green</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-alternative-deployment-strategies">Alternative Deployment Strategies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cleanup">Cleanup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-further-resources">Further Resources</a></p>
</li>
</ul>
<h2 id="heading-the-problem-with-state-in-blue-green-deployments">The Problem with State in Blue-Green Deployments</h2>
<p>The elegance of blue-green deployments starts to crumble when you consider databases. Here's why: your blue environment runs application version 1, your green environment runs version 2, but they both connect to the same RDS instance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768056130585/109ceff8-4500-45d7-aaa0-5e259b4a7b11.png" alt="Figure 1: The blue-green dilemma - both environments share the same database but expect different schemas" class="image--center mx-auto" width="1579" height="1131" loading="lazy"></p>
<p>Consider this scenario: you're adding a new feature that requires a new database column. Version 2 of your application expects this column to exist. You deploy green, run your migration to add the column, and switch traffic.</p>
<p>Everything works great until you need to rollback. Now version 1 is receiving traffic, but it doesn't know what to do with that new column. Worse, if your migration removed or renamed a column that version 1 depends on, your rollback will fail catastrophically.</p>
<p>Here are the specific challenges you'll face:</p>
<ul>
<li><p><strong>Schema versioning conflicts</strong>: Your blue environment expects schema version N, while green expects version N+1. Any breaking schema change will cause one environment to fail.</p>
</li>
<li><p><strong>Data inconsistencies</strong>: If version 2 writes data in a new format that version 1 can't read, switching back to blue will result in errors or data corruption.</p>
</li>
<li><p><strong>Irreversible migrations</strong>: Some database changes are inherently destructive. Dropping a column, changing data types, or restructuring tables can't be easily undone.</p>
</li>
<li><p><strong>Failed rollbacks</strong>: The promise of instant rollback becomes hollow when your database has evolved beyond what the blue environment can handle.</p>
</li>
</ul>
<p>Let's explore the strategies that solve these problems.</p>
<h2 id="heading-database-migration-strategies-for-blue-green">Database Migration Strategies for Blue-Green</h2>
<h3 id="heading-strategy-1-the-expand-contract-pattern-recommended">Strategy 1: The Expand-Contract Pattern (Recommended)</h3>
<p>The expand-contract pattern is the most practical approach for blue-green deployments with shared databases. It works by breaking schema changes into three phases, ensuring backwards compatibility throughout.</p>
<h4 id="heading-phase-1-expand">Phase 1: Expand</h4>
<p>In this phase, you add new schema elements while keeping old ones intact. If you're renaming a column, add the new column without removing the old one. If you're changing table structure, create new tables alongside existing ones.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Example: Renaming 'user_name' to 'username'</span>
<span class="hljs-comment">-- Phase 1: Expand - Add new column</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span> <span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span> username <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>);

<span class="hljs-comment">-- Populate new column from old column</span>
<span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">users</span> <span class="hljs-keyword">SET</span> username = user_name <span class="hljs-keyword">WHERE</span> username <span class="hljs-keyword">IS</span> <span class="hljs-literal">NULL</span>;
</code></pre>
<p>At this point, your database supports both the old schema (used by blue) and the new schema (used by green). Your application code needs to handle both as well.</p>
<h4 id="heading-phase-2-deploy">Phase 2: Deploy</h4>
<p>Now, deploy your green environment with code that uses the new schema. But this code should still write to both old and new columns to maintain compatibility.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Version 2 code - writes to both columns</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_user</span>(<span class="hljs-params">user_id, username</span>):</span>
    db.execute(
        <span class="hljs-string">"UPDATE users SET username = %s, user_name = %s WHERE id = %s"</span>,
        (username, username, user_id)
    )
</code></pre>
<p>Traffic shifts from blue to green. Both environments work because the database supports both schemas. If you need to rollback, blue still functions perfectly because the old columns are intact.</p>
<h4 id="heading-phase-3-contract">Phase 3: Contract</h4>
<p>After you're confident green is stable and you've decommissioned blue, remove the old schema elements in a separate deployment.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Phase 3: Contract - Remove old column</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span> <span class="hljs-keyword">DROP</span> <span class="hljs-keyword">COLUMN</span> user_name;
</code></pre>
<p>Update your application code to stop writing to the old columns. This is now version 3, deployed as a standard release.</p>
<p><strong>When to use</strong>: This should be your default approach for most schema changes including adding/removing columns, renaming fields, changing constraints, and restructuring tables.</p>
<h3 id="heading-strategy-2-parallel-schemas-or-databases">Strategy 2: Parallel Schemas or Databases</h3>
<p>For major breaking changes where backwards compatibility is impractical, you might maintain entirely separate database versions. Version 1 connects to database A, version 2 connects to database B. This approach requires data synchronization between databases. AWS Database Migration Service (DMS) can replicate data in near real-time, or you can build custom replication logic using change data capture.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Configuration for version-specific database connections</span>
DATABASE_CONFIG = {
    <span class="hljs-string">'v1'</span>: {
        <span class="hljs-string">'host'</span>: <span class="hljs-string">'blue-db.cluster-xxxxx.us-east-1.rds.amazonaws.com'</span>,
        <span class="hljs-string">'database'</span>: <span class="hljs-string">'app_v1'</span>
    },
    <span class="hljs-string">'v2'</span>: {
        <span class="hljs-string">'host'</span>: <span class="hljs-string">'green-db.cluster-yyyyy.us-east-1.rds.amazonaws.com'</span>,
        <span class="hljs-string">'database'</span>: <span class="hljs-string">'app_v2'</span>
    }
}
</code></pre>
<p>During the transition period, you run DMS to keep both databases synchronized, with the understanding that writes go to the active version's database.</p>
<p>The challenge is that you're now managing data synchronization, dealing with replication lag, and paying for two databases. Eventually, you need to consolidate back to one database, which requires another migration. This is expensive and complex, which is why it's the "nuclear option."</p>
<p><strong>When to use</strong>: Only for major architectural changes, complete data model redesigns, or when migrating between database types (for example, MySQL to PostgreSQL). If expand-contract can possibly work, use that instead.</p>
<h3 id="heading-strategy-3-feature-flags-for-gradual-rollout">Strategy 3: Feature Flags for Gradual Rollout</h3>
<p>Feature flags allow you to decouple deployment from release. Both blue and green run the same codebase, but features are toggled on or off via configuration. This shifts the problem from schema compatibility to code-level compatibility.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_user</span>(<span class="hljs-params">user_data</span>):</span>
    config = get_feature_config()
    <span class="hljs-keyword">if</span> config[<span class="hljs-string">'use_new_user_schema'</span>]:
        <span class="hljs-keyword">return</span> create_user_v2(user_data)
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">return</span> create_user_v1(user_data)
</code></pre>
<p>Instead of having two separate deployments (blue and green), you have ONE deployment with conditional logic. The "switch" from old to new behavior happens via configuration change, not infrastructure change. This is technically not pure blue-green, but it's a powerful hybrid approach.</p>
<h4 id="heading-how-it-works">How it works</h4>
<p>Your application checks AWS AppConfig (or similar service) for feature flags before executing code paths. When a flag is off, it uses the old schema/logic. When on, it uses the new schema/logic. You can even enable features for a percentage of users (5% get new behavior, 95% get old behavior) for gradual rollout.</p>
<p>The tradeoff is that your codebase temporarily contains both old and new logic with conditional branches everywhere. This increases complexity and requires disciplined cleanup after the feature is fully released. However, you gain fine-grained control and can toggle features on/off instantly without deploying new infrastructure.</p>
<p><strong>When to use:</strong> For large features with uncertain stability, gradual rollouts to monitor impact, or when you want instant rollback capability without touching infrastructure. Also useful when combined with expand-contract for extra safety.</p>
<h2 id="heading-handling-stateful-services-in-ecs">Handling Stateful Services in ECS</h2>
<p>Beyond databases, several other stateful components require careful consideration during blue-green deployments.</p>
<h3 id="heading-session-management">Session Management</h3>
<p>It’s a good idea to store sessions in ElastiCache or DynamoDB rather than application memory:</p>
<pre><code class="lang-python">app.config[<span class="hljs-string">'SESSION_TYPE'</span>] = <span class="hljs-string">'dynamodb'</span>
app.config[<span class="hljs-string">'SESSION_DYNAMODB'</span>] = boto3.client(<span class="hljs-string">'dynamodb'</span>)
</code></pre>
<h3 id="heading-shared-resources">Shared Resources</h3>
<p>Beyond database sessions, your application likely depends on other stateful components that need coordination during blue-green deployments:</p>
<h4 id="heading-1-s3-buckets">1. S3 buckets</h4>
<p>If your application stores files or data in S3, schema changes to object metadata or file formats can cause compatibility issues between versions. To address this, you can enable S3 versioning to maintain multiple format versions simultaneously.</p>
<p>For example, if version 2 writes JSON files with a new structure, version 1 should still be able to read the old format. You can include a version prefix in object keys (like <code>v1/user-data.json</code> and <code>v2/user-data.json</code>) or embed version metadata in the objects themselves.</p>
<h4 id="heading-message-queues-sqssns">Message queues (SQS/SNS)</h4>
<p>Messages sent by one version must be readable by the other during the transition. You can use versioned message schemas with a <code>schema_version</code> field in your message payload. Both blue and green should be able to parse messages from either version, even if they only produce messages in their preferred format. Consider using a schema registry or validation library to ensure compatibility.</p>
<h4 id="heading-cache-layers-elasticacheredis">Cache layers (ElastiCache/Redis)</h4>
<p>Cached data structure changes can cause deserialization errors when switching between versions. Try versioning your cache keys by including the schema version: <code>CACHE_VERSION = 'v2'</code> and then <code>cache_key = f"user:{CACHE_VERSION}:{user_id}"</code>. This ensures blue and green maintain separate cache namespaces, preventing cross-contamination. When you fully migrate to green, you can flush the old cache keys or let them expire naturally.</p>
<pre><code class="lang-python">CACHE_VERSION = <span class="hljs-string">'v2'</span>
cache_key = <span class="hljs-string">f"user:<span class="hljs-subst">{CACHE_VERSION}</span>:<span class="hljs-subst">{user_id}</span>"</span>
</code></pre>
<h2 id="heading-implementation-end-to-end-example">Implementation: End-to-End Example</h2>
<p>Let's walk through a complete blue-green deployment with ECS, handling a database schema change using the <strong>expand-contract pattern</strong>. We'll migrate from a single <code>address</code> text field to structured <code>street_address</code>, <code>city</code>, <code>state</code>, and <code>zip_code</code> fields.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768052075044/fdb732dd-cf3d-473f-a22c-f5ab98870625.png" alt="Figure 2: The three phases of expand-contract migration ensuring continuous compatibility" class="image--center mx-auto" width="3444" height="624" loading="lazy"></p>
<p><strong>Here’s the scenario:</strong> You're running an e-commerce application on ECS. The current version (blue) stores customer addresses in a single address text field. Version 2 (green) splits this into structured fields: street_address, city, state, and zip_code.</p>
<h3 id="heading-architecture-setup"><strong>Architecture Setup</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768087707691/ff19ce97-b745-4aa8-8b39-4d835fd781cd.png" alt="Figure 3: Complete AWS architecture for blue-green ECS deployment with shared RDS database" class="image--center mx-auto" width="2479" height="3679" loading="lazy"></p>
<p>Your infrastructure includes:</p>
<ul>
<li><p>ECS cluster running Fargate tasks</p>
</li>
<li><p>Application Load Balancer with two target groups (blue and green)</p>
</li>
<li><p>RDS PostgreSQL database (shared between environments)</p>
</li>
<li><p>CodeDeploy for managing traffic shifts</p>
</li>
<li><p>Parameter Store for database connection strings</p>
</li>
</ul>
<p>💡 <strong>Implementation Note</strong>: The complete Terraform code for this architecture is available in the <a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs/tree/main/terraform">companion GitHub repository</a>.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before starting, make sure that you have the following tools installed and your AWS credentials properly configured:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Required tools</span>
aws --version      <span class="hljs-comment"># AWS CLI</span>
terraform --version <span class="hljs-comment"># Terraform &gt;= 1.0</span>
docker --version   <span class="hljs-comment"># Docker</span>
psql --version     <span class="hljs-comment"># PostgreSQL client</span>

<span class="hljs-comment"># Configure AWS credentials</span>
aws configure
aws sts get-caller-identity  <span class="hljs-comment"># Verify your identity</span>
</code></pre>
<h3 id="heading-step-1-deploy-infrastructure-and-blue-environment">Step 1: Deploy Infrastructure and Blue Environment</h3>
<p>We’ll start by setting up the entire AWS infrastructure from scratch using Terraform, then deploying the initial version of our application (blue environment).</p>
<p>First, clone the repository and set up your environment:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Clone the repository</span>
git <span class="hljs-built_in">clone</span> https://github.com/Caesarsage/bluegreen-deployment-ecs.git
<span class="hljs-built_in">cd</span> bluegreen-deployment-ecs

<span class="hljs-comment"># Create terraform variables</span>
<span class="hljs-built_in">cd</span> terraform
cat &gt; terraform.tfvars &lt;&lt;EOF
aws_region         = <span class="hljs-string">"us-east-1"</span>
project_name       = <span class="hljs-string">"ecommerce-bluegreen"</span>
environment        = <span class="hljs-string">"production"</span>
vpc_cidr           = <span class="hljs-string">"10.0.0.0/16"</span>

<span class="hljs-comment"># Database credentials (CHANGE THESE!)</span>
db_username = <span class="hljs-string">"dbadmin"</span>
db_password = <span class="hljs-string">"ChangeThisPassword123!"</span>

<span class="hljs-comment"># Container configuration</span>
container_image = <span class="hljs-string">"PLACEHOLDER"</span>  <span class="hljs-comment"># Will update after building image</span>
container_port  = 8080

<span class="hljs-comment"># Scaling configuration</span>
desired_count = 2
cpu           = <span class="hljs-string">"256"</span>
memory        = <span class="hljs-string">"512"</span>

<span class="hljs-comment"># Notifications</span>
notification_email = <span class="hljs-string">"your-email@example.com"</span>
EOF
</code></pre>
<p><strong>Security Note:</strong> Never commit <code>terraform.tfvars</code> to Git. It's already in <code>.gitignore</code>.</p>
<p>Next, initialize Terraform and create the ECR repository:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Initialize Terraform</span>
terraform init
terraform validate

<span class="hljs-comment"># Create ECR repository</span>
terraform apply -target=aws_ecr_repository.app

<span class="hljs-comment"># Get ECR repository URL</span>
<span class="hljs-built_in">export</span> ECR_REPO=$(terraform output -raw ecr_repository_url)
<span class="hljs-built_in">echo</span> <span class="hljs-string">"ECR Repository: <span class="hljs-variable">$ECR_REPO</span>"</span>
</code></pre>
<p>We create the ECR repository first because we need somewhere to push our Docker image. Then we'll build the image, push it, and finally deploy the rest of the infrastructure that depends on that image existing.</p>
<p>Build and push the initial application like this:</p>
<pre><code class="lang-bash">
<span class="hljs-built_in">cd</span> ..  <span class="hljs-comment"># Back to project root</span>

<span class="hljs-comment"># Set variables</span>
<span class="hljs-built_in">export</span> AWS_REGION=us-east-1
<span class="hljs-built_in">export</span> AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
<span class="hljs-built_in">export</span> ECR_REPOSITORY=ecommerce-bluegreen
<span class="hljs-built_in">export</span> IMAGE_TAG=v1.0.0

<span class="hljs-comment"># Login to ECR</span>
aws ecr get-login-password --region <span class="hljs-variable">$AWS_REGION</span> | \
    docker login --username AWS --password-stdin <span class="hljs-variable">$AWS_ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com

<span class="hljs-comment"># Build the image</span>
docker build --platform linux/amd64 -t <span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span> -f docker/Dockerfile .

<span class="hljs-comment"># Tag and push to ECR</span>
docker tag <span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span> \
    <span class="hljs-variable">$AWS_ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span>

docker push <span class="hljs-variable">$AWS_ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span>

<span class="hljs-comment"># Update terraform.tfvars with the image URL</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"container_image = \"<span class="hljs-variable">$AWS_ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span>\""</span> &gt;&gt; terraform/terraform.tfvars
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768137809806/820d7005-b924-4224-9b58-de5701466c1f.png" alt="Figure 4: ECR Private repository for Docker image" class="image--center mx-auto" width="2442" height="632" loading="lazy"></p>
<p>The <a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs/tree/main/app">application code</a> is a Flask application that handles both old and new schema formats based on the <code>APP_VERSION</code> environment variable.</p>
<p>Now deploy the complete infrastructure:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> terraform
terraform apply  <span class="hljs-comment"># Takes ~15-20 minutes</span>

<span class="hljs-comment"># Get outputs</span>
<span class="hljs-built_in">export</span> ALB_URL=$(terraform output -raw alb_url)
<span class="hljs-built_in">export</span> TEST_URL=$(terraform output -raw test_url)
<span class="hljs-built_in">export</span> DB_ENDPOINT=$(terraform output -raw db_endpoint)
<span class="hljs-built_in">export</span> ECR_URL=$(terraform output -raw ecr_repository_url)
<span class="hljs-built_in">export</span> BASTION_IP=$(terraform output -raw bastion_public_ip)

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Application URL: <span class="hljs-variable">$ALB_URL</span>"</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Test URL: <span class="hljs-variable">$TEST_URL</span>"</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Database Endpoint: <span class="hljs-variable">$DB_ENDPOINT</span>"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768141033921/07c2e9b9-c652-4cec-91ae-2de956d8655d.png" alt="Application Load Balancer with two target groups (blue and green)" class="image--center mx-auto" width="2504" height="844" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768142296716/9963c779-e0a8-4418-8d69-9bc8fcbbc553.png" alt="Figure 5: Application Load Balancer with two target groups (blue and green)" class="image--center mx-auto" width="2553" height="458" loading="lazy"></p>
<p>The production listener (port 80) is what your users hit. The test listener (port 8080) lets you test the green environment before shifting production traffic to it. This is crucial for validation.</p>
<p>You can see the complete Terraform configuration in <a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs/tree/main/terraform"><code>terraform</code></a>.</p>
<h3 id="heading-step-2-initialize-database-schema">Step 2: Initialize Database Schema</h3>
<p>Now you’ll need to initialize the database with the schema for version 1 (blue). We'll use Bastion for secure access:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Copy the migration files to the bastion host from your local machine</span>

scp -i ~/.ssh/id_rsa docker/init.sql ec2-user@<span class="hljs-variable">$BASTION_IP</span>:/tmp/
scp -i ~/.ssh/id_rsa migrations/*.sql ec2-user@<span class="hljs-variable">$BASTION_IP</span>:/tmp/

<span class="hljs-comment"># Then SSH into it and run migrations</span>
ssh -i ~/.ssh ec2-user@<span class="hljs-variable">$BASTION_IP</span>

<span class="hljs-comment"># Inside the bastion:</span>
psql -h <span class="hljs-variable">$DB_ENDPOINT</span> -U dbadmin -d ecommerce -f /tmp/init.sql

<span class="hljs-comment"># Verify</span>
psql -h <span class="hljs-variable">$DB_HOST</span> -U <span class="hljs-variable">$DB_USER</span> -d <span class="hljs-variable">$DB_NAME</span> -c <span class="hljs-string">"\d customers"</span>

<span class="hljs-comment"># Exit the container</span>
<span class="hljs-built_in">exit</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768089062401/8f23655e-b50b-4b24-af98-b195e29da9c7.png" alt="Figure 6: Database schema - the customers table with the original columns" class="image--center mx-auto" width="1298" height="402" loading="lazy"></p>
<h3 id="heading-step-3-verify-blue-environment">Step 3: Verify Blue Environment</h3>
<p>We’ll want to test that everything works before we start the migration. This is your baseline: you want to confirm that the current system is healthy before introducing changes.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check health</span>
curl <span class="hljs-variable">$ALB_URL</span>/health | jq

<span class="hljs-comment"># Expected response:</span>
<span class="hljs-comment"># {</span>
<span class="hljs-comment">#   "status": "healthy",</span>
<span class="hljs-comment">#   "version": "blue",</span>
<span class="hljs-comment">#   "environment": "production",</span>
<span class="hljs-comment">#   "database": "connected",</span>
<span class="hljs-comment">#   "schema": "compatible"</span>
<span class="hljs-comment"># }</span>

<span class="hljs-comment"># Create a customer with the old schema (single address field)</span>
curl -X POST <span class="hljs-variable">$ALB_URL</span>/api/customers \
    -H <span class="hljs-string">"Content-Type: application/json"</span> \
    -d <span class="hljs-string">'{
      "name": "John Doe",
      "email": "john@example.com",
      "address": "123 Main St, New York, NY, 10001"
    }'</span> | jq

<span class="hljs-comment"># List customers</span>
curl <span class="hljs-variable">$ALB_URL</span>/api/customers | jq
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768138569485/b7455a6e-b101-4cdb-83b8-40e0dbafb0b0.png" alt="Figure 7: Blue Environment Verification" class="image--center mx-auto" width="1068" height="434" loading="lazy"></p>
<h3 id="heading-step-4-expand-phase-add-new-columns">Step 4: Expand Phase – Add New Columns</h3>
<p>This is the first phase of expand-contract. We're adding the new columns WITHOUT removing the old one, creating a database schema that supports both blue and green simultaneously.</p>
<p>Run the expand migration (<a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs/blob/main/migrations/001_expand_address.sql"><code>migrations/001_expand_address.sql</code>)</a>:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Migration: 001_expand_address_fields.sql</span>
<span class="hljs-keyword">BEGIN</span>;

<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> customers 
  <span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span> street_address <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>),
  <span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span> city <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>),
  <span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span> state <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">2</span>),
  <span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span> zip_code <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">10</span>);

<span class="hljs-comment">-- Populate new columns from existing data</span>
<span class="hljs-comment">-- This uses a simple parsing strategy; yours might be more sophisticated</span>

<span class="hljs-keyword">UPDATE</span> customers 
<span class="hljs-keyword">SET</span> 
  street_address = SPLIT_PART(address, <span class="hljs-string">','</span>, <span class="hljs-number">1</span>),
  city = <span class="hljs-keyword">TRIM</span>(SPLIT_PART(address, <span class="hljs-string">','</span>, <span class="hljs-number">2</span>)),
  state = <span class="hljs-keyword">TRIM</span>(SPLIT_PART(address, <span class="hljs-string">','</span>, <span class="hljs-number">3</span>)),
  zip_code = <span class="hljs-keyword">TRIM</span>(SPLIT_PART(address, <span class="hljs-string">','</span>, <span class="hljs-number">4</span>))
<span class="hljs-keyword">WHERE</span> address <span class="hljs-keyword">IS</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>;

<span class="hljs-keyword">COMMIT</span>;
</code></pre>
<p><strong>Critical observation:</strong> We're NOT dropping the <code>address</code> column. It's still there. Blue continues reading and writing to it, completely unaware that new columns exist. This is what makes the migration safe – nothing breaks.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Then SSH into it and run migrations</span>
ssh -i ~/.ssh ec2-user@<span class="hljs-variable">$BASTION_IP</span>

<span class="hljs-comment"># Inside the bastion:</span>
<span class="hljs-built_in">export</span> DB_ENDPOINT = <span class="hljs-string">""</span> <span class="hljs-comment"># from terraform output</span>

psql -h <span class="hljs-variable">$DB_ENDPOINT</span> -U dbadmin -d ecommerce -f /tmp/001_expand_address.sql

<span class="hljs-comment"># Verify new columns exist</span>
psql -h <span class="hljs-variable">$DB_ENDPOINT</span> -U dbadmin -d ecommerce -c <span class="hljs-string">"\d customers"</span>

<span class="hljs-built_in">exit</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768089194050/e053dee3-382b-4ccd-a0e0-8c17003e9832.png" alt="Figure 8: Database schema evolution - the customers table during expand phase with both old and new columns" class="image--center mx-auto" width="1638" height="694" loading="lazy"></p>
<p><strong>Verification:</strong> The <code>\d customers</code> command shows the table structure. You should see BOTH the old <code>address</code> column AND the new <code>street_address</code>, <code>city</code>, <code>state</code>, <code>zip_code</code> columns. This confirms the expand phase worked.</p>
<p>The database now supports both old (blue) and new (green) schemas. Blue is still running and working perfectly, and nothing has changed from its perspective.</p>
<h3 id="heading-step-5-build-and-deploy-green-environment">Step 5: Build and Deploy Green Environment</h3>
<p>Now we’ll build version 2 of our application that knows how to work with the new structured address fields, while maintaining backwards compatibility with the old schema.</p>
<p>Start by building version 2 with structured address support:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> ..  <span class="hljs-comment"># Back to project root</span>

<span class="hljs-comment"># Build new version</span>
<span class="hljs-built_in">export</span> IMAGE_TAG=v2.0.0

docker build --platform linux/amd64 -t <span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span> -f docker/Dockerfile .

docker tag <span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span> \
    <span class="hljs-variable">$AWS_ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span>

docker push <span class="hljs-variable">$AWS_ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPOSITORY</span>:<span class="hljs-variable">$IMAGE_TAG</span>
</code></pre>
<p>What’s different is that the v2 <a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs/blob/main/app/models.py">application code</a> now has logic that:</p>
<ul>
<li><p><strong>Reads</strong> from the new structured columns (<code>street_address</code>, <code>city</code>, and so on)</p>
</li>
<li><p><strong>Writes</strong> to BOTH new columns AND the old <code>address</code> column</p>
</li>
<li><p>Accepts API requests with structured address format</p>
</li>
</ul>
<p><strong>Why write to both:</strong> This is crucial. Even though green prefers the new format, it maintains the old format, too. If you need to rollback to blue, all the data blue needs is there and up-to-date. Without this, rollback would be impossible: blue would see empty or stale <code>address</code> fields.</p>
<p>Now create and register green task definition:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> terraform

<span class="hljs-comment"># Get necessary ARNs</span>
EXECUTION_ROLE_ARN=$(terraform output -raw ecs_task_execution_role_arn)
TASK_ROLE_ARN=$(terraform output -raw ecs_task_role_arn)
DB_SECRET_ARN=$(terraform output -raw db_secret_arn)

<span class="hljs-comment"># Create task definition</span>
cat &gt; task-def-green.json &lt;&lt;EOF
{
  <span class="hljs-string">"family"</span>: <span class="hljs-string">"ecommerce-bluegreen"</span>,
  <span class="hljs-string">"networkMode"</span>: <span class="hljs-string">"awsvpc"</span>,
  <span class="hljs-string">"requiresCompatibilities"</span>: [<span class="hljs-string">"FARGATE"</span>],
  <span class="hljs-string">"cpu"</span>: <span class="hljs-string">"256"</span>,
  <span class="hljs-string">"memory"</span>: <span class="hljs-string">"512"</span>,
  <span class="hljs-string">"executionRoleArn"</span>: <span class="hljs-string">"<span class="hljs-variable">${EXECUTION_ROLE_ARN}</span>"</span>,
  <span class="hljs-string">"taskRoleArn"</span>: <span class="hljs-string">"<span class="hljs-variable">${TASK_ROLE_ARN}</span>"</span>,
  <span class="hljs-string">"containerDefinitions"</span>: [{
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"app"</span>,
    <span class="hljs-string">"image"</span>: <span class="hljs-string">"<span class="hljs-variable">${AWS_ACCOUNT_ID}</span>.dkr.ecr.<span class="hljs-variable">${AWS_REGION}</span>.amazonaws.com/<span class="hljs-variable">${ECR_REPOSITORY}</span>:<span class="hljs-variable">${IMAGE_TAG}</span>"</span>,
    <span class="hljs-string">"essential"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"portMappings"</span>: [{
      <span class="hljs-string">"containerPort"</span>: 8080,
      <span class="hljs-string">"protocol"</span>: <span class="hljs-string">"tcp"</span>
    }],
    <span class="hljs-string">"environment"</span>: [
      {<span class="hljs-string">"name"</span>: <span class="hljs-string">"APP_VERSION"</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"green"</span>},
      {<span class="hljs-string">"name"</span>: <span class="hljs-string">"ENVIRONMENT"</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"production"</span>},
      {<span class="hljs-string">"name"</span>: <span class="hljs-string">"AWS_REGION"</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"<span class="hljs-variable">${AWS_REGION}</span>"</span>},
      {<span class="hljs-string">"name"</span>: <span class="hljs-string">"DB_HOST"</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"<span class="hljs-variable">${DB_ENDPOINT}</span>"</span>},
      {<span class="hljs-string">"name"</span>: <span class="hljs-string">"DB_PORT"</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"5432"</span>},
      {<span class="hljs-string">"name"</span>: <span class="hljs-string">"DB_NAME"</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"ecommerce"</span>}
    ],
    <span class="hljs-string">"secrets"</span>: [
      {
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"DB_USER"</span>,
        <span class="hljs-string">"valueFrom"</span>: <span class="hljs-string">"<span class="hljs-variable">${DB_SECRET_ARN}</span>:username::"</span>
      },
      {
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"DB_PASSWORD"</span>,
        <span class="hljs-string">"valueFrom"</span>: <span class="hljs-string">"<span class="hljs-variable">${DB_SECRET_ARN}</span>:password::"</span>
      }
    ],
    <span class="hljs-string">"logConfiguration"</span>: {
      <span class="hljs-string">"logDriver"</span>: <span class="hljs-string">"awslogs"</span>,
      <span class="hljs-string">"options"</span>: {
        <span class="hljs-string">"awslogs-group"</span>: <span class="hljs-string">"/ecs/ecommerce-bluegreen"</span>,
        <span class="hljs-string">"awslogs-region"</span>: <span class="hljs-string">"<span class="hljs-variable">${AWS_REGION}</span>"</span>,
        <span class="hljs-string">"awslogs-stream-prefix"</span>: <span class="hljs-string">"ecs"</span>
      }
    },
    <span class="hljs-string">"healthCheck"</span>: {
      <span class="hljs-string">"command"</span>: [<span class="hljs-string">"CMD-SHELL"</span>, <span class="hljs-string">"curl -f http://localhost:8080/health || exit 1"</span>],
      <span class="hljs-string">"interval"</span>: 30,
      <span class="hljs-string">"timeout"</span>: 5,
      <span class="hljs-string">"retries"</span>: 3,
      <span class="hljs-string">"startPeriod"</span>: 60
    }
  }]
}
EOF

<span class="hljs-comment"># Register the task definition</span>
aws ecs register-task-definition --cli-input-json file://task-def-green.json
</code></pre>
<p>This JSON tells ECS everything about how to run your container:</p>
<ul>
<li><p>Which Docker image to use (the v2.0.0 we just built)</p>
</li>
<li><p>How much CPU/memory to allocate (256 CPU units = 0.25 vCPU)</p>
</li>
<li><p>Environment variables (notice <code>APP_VERSION</code> is set to "green")</p>
</li>
<li><p>Secrets (database credentials pulled from AWS Secrets Manager)</p>
</li>
<li><p>Health check configuration (curl the /health endpoint every 30 seconds)</p>
</li>
<li><p>Logging configuration (send logs to CloudWatch)</p>
</li>
</ul>
<p><strong>Key detail:</strong> The <code>APP_VERSION</code> environment variable is how the application knows whether to behave as blue or green. Same codebase, different behavior based on configuration.</p>
<h3 id="heading-step-6-execute-blue-green-deployment">Step 6: Execute Blue-Green Deployment</h3>
<p>Alright, now it’s time to create AppSpec and trigger the deployment:</p>
<pre><code class="lang-bash">TASK_DEF_ARN=$(aws ecs describe-task-definition \
  --task-definition ecommerce-bluegreen \
  --query <span class="hljs-string">'taskDefinition.taskDefinitionArn'</span> \
  --output text)

cat &gt; appspec.json &lt;&lt;EOF
{
  <span class="hljs-string">"version"</span>: 0.0,
  <span class="hljs-string">"Resources"</span>: [{
    <span class="hljs-string">"TargetService"</span>: {
      <span class="hljs-string">"Type"</span>: <span class="hljs-string">"AWS::ECS::Service"</span>,
      <span class="hljs-string">"Properties"</span>: {
        <span class="hljs-string">"TaskDefinition"</span>: <span class="hljs-string">"<span class="hljs-variable">${TASK_DEF_ARN}</span>"</span>,
        <span class="hljs-string">"LoadBalancerInfo"</span>: {
          <span class="hljs-string">"ContainerName"</span>: <span class="hljs-string">"app"</span>,
          <span class="hljs-string">"ContainerPort"</span>: 8080
        }
      }
    }
  }]
}
EOF

<span class="hljs-comment"># Deploy</span>
APPSPEC=$(cat appspec.json | jq -c .)
aws deploy create-deployment \
  --application-name ecommerce-bluegreen \
  --deployment-group-name ecommerce-bluegreen-deployment-group \
  --deployment-config-name CodeDeployDefault.ECSLinear10PercentEvery3Minutes \
  --description <span class="hljs-string">"Blue-green deployment to structured address schema"</span> \
  --cli-input-json <span class="hljs-string">"{
    \"revision\": {
      \"revisionType\": \"AppSpecContent\",
      \"appSpecContent\": {
        \"content\": <span class="hljs-subst">$(echo \"$APPSPEC\" | jq -Rs .)</span>
      }
    }
  }"</span>

DEPLOYMENT_ID=$(aws deploy list-deployments \
    --application-name ecommerce-bluegreen \
    --deployment-group-name ecommerce-bluegreen-deployment-group \
    --query <span class="hljs-string">'deployments[0]'</span> --output text)
</code></pre>
<p>Monitor the deployment:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Watch status</span>
watch -n 10 <span class="hljs-string">"aws deploy get-deployment --deployment-id <span class="hljs-variable">$DEPLOYMENT_ID</span> \
    --query 'deploymentInfo.status' --output text"</span>

<span class="hljs-comment"># Monitor traffic distribution</span>
<span class="hljs-keyword">while</span> <span class="hljs-literal">true</span>; <span class="hljs-keyword">do</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Production: <span class="hljs-subst">$(curl -s $ALB_URL/health | jq -r '.version')</span>"</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Test: <span class="hljs-subst">$(curl -s $TEST_URL/health | jq -r '.version')</span>"</span>
    sleep 30
<span class="hljs-keyword">done</span>
</code></pre>
<p>The deployment shifts 10% of traffic every 3 minutes, completing in 30 minutes.</p>
<h3 id="heading-step-7-validate-green-environment">Step 7: Validate Green Environment</h3>
<p>After the deployment begins, you need to validate that the green environment is functioning correctly with the new structured address format before allowing production traffic to reach it.</p>
<p>The CodeBuild dashboard below shows the Traffic migration and Deployment status:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768093087711/fc1b869c-7fae-421e-8d98-45769300cb0a.png" alt="Monitoring in CodeDeploy" class="image--center mx-auto" width="2282" height="1460" loading="lazy"></p>
<p>We can also test through the test listener (port 8080), which provides isolated access to green tasks:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Test new structured address API</span>
curl -X POST <span class="hljs-variable">$TEST_URL</span>/api/customers \
    -H <span class="hljs-string">"Content-Type: application/json"</span> \
    -d <span class="hljs-string">'{
      "name": "Jane Smith",
      "email": "jane@example.com",
      "address": {
        "street": "456 Oak Ave",
        "city": "Los Angeles",
        "state": "CA",
        "zip": "90001"
      }
    }'</span> | jq

curl <span class="hljs-variable">$ALB_URL</span>/api/customers | jq
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768140730325/57c6a047-994f-4b5e-8e19-4d6fb25ad44e.png" alt="Validate Green environment response" class="image--center mx-auto" width="1422" height="672" loading="lazy"></p>
<p>What you're validating:</p>
<ul>
<li><p>The green environment accepts the new structured address format</p>
</li>
<li><p>Data is correctly written to both new columns (street_address, city, state, zip_code) and the old address column for backwards compatibility</p>
</li>
<li><p>The API response matches expectations for the new schema</p>
</li>
<li><p>Existing data from blue environment is still accessible and readable</p>
</li>
</ul>
<p>If any of these tests fail, you can stop the deployment before production traffic reaches green, preventing customer impact.</p>
<h3 id="heading-step-8-post-deployment-validation">Step 8: Post-Deployment Validation</h3>
<p>Once CodeDeploy completes the traffic shift, all production requests route to green. This is your opportunity to verify that the deployment was successful and that the new version is handling real production traffic correctly.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Verify all production traffic goes to green</span>
<span class="hljs-comment"># Running this multiple times confirms consistent routing</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..10}; <span class="hljs-keyword">do</span>
    curl -s <span class="hljs-variable">$ALB_URL</span>/health | jq -r <span class="hljs-string">'.version'</span>
<span class="hljs-keyword">done</span>
<span class="hljs-comment"># Expected output: "green" for all 10 requests</span>

<span class="hljs-comment"># Test complete CRUD operations with the new API</span>
<span class="hljs-comment"># Create a customer with structured address</span>
CUSTOMER_ID=$(curl -s -X POST <span class="hljs-variable">$ALB_URL</span>/api/customers \
    -H <span class="hljs-string">"Content-Type: application/json"</span> \
    -d <span class="hljs-string">'{"name": "Test User", "email": "test@example.com",
         "address": {"street": "789 Test St", "city": "Test City", 
         "state": "TX", "zip": "75001"}}'</span> | jq -r <span class="hljs-string">'.id'</span>)

<span class="hljs-comment"># Read the customer back to verify data persistence</span>
curl <span class="hljs-variable">$ALB_URL</span>/api/customers/<span class="hljs-variable">$CUSTOMER_ID</span> | jq

<span class="hljs-comment"># Update the customer to test modification</span>
curl -X PUT <span class="hljs-variable">$ALB_URL</span>/api/customers/<span class="hljs-variable">$CUSTOMER_ID</span> \
    -H <span class="hljs-string">"Content-Type: application/json"</span> \
    -d <span class="hljs-string">'{"address": {"street": "999 Updated Ave", "city": "Test City", 
         "state": "TX", "zip": "75001"}}'</span> | jq

<span class="hljs-comment"># Delete the test customer for cleanup</span>
curl -X DELETE <span class="hljs-variable">$ALB_URL</span>/api/customers/<span class="hljs-variable">$CUSTOMER_ID</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768140850962/a31273e9-cbc1-4d09-9f6d-7248b402f712.png" alt="Verify all production traffic goes to green" class="image--center mx-auto" width="846" height="270" loading="lazy"></p>
<p>What you're validating:</p>
<ul>
<li><p>Traffic routing is 100% to green with no requests reaching blue</p>
</li>
<li><p>Create operations work with the new structured address format</p>
</li>
<li><p>Read operations return correct data with proper address structure</p>
</li>
<li><p>Update operations successfully modify existing records</p>
</li>
<li><p>Delete operations work without errors</p>
</li>
<li><p>The application correctly writes to both new columns and old address column (enabling potential rollback)</p>
</li>
</ul>
<p>Check your CloudWatch logs and metrics during this validation period for any unexpected errors, increased latency, or database connection issues.</p>
<h3 id="heading-step-9-contract-phase-after-24-72-hours">Step 9: Contract Phase (After 24-72 Hours)</h3>
<p>This is the final phase of expand-contract. We're removing the old <code>address</code> column now that we're confident green is stable. This is the point of no return.</p>
<p><strong>CRITICAL</strong>: Only proceed after green has been stable for your confidence period!</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Backup database first</span>
aws rds create-db-snapshot \
    --db-instance-identifier ecommerce-bluegreen-db \
    --db-snapshot-identifier pre-contract-$(date +%Y%m%d-%H%M%S)

<span class="hljs-comment"># Wait for snapshot</span>
aws rds <span class="hljs-built_in">wait</span> db-snapshot-completed \
    --db-snapshot-identifier pre-contract-$(date +%Y%m%d-%H%M%S)

<span class="hljs-comment"># Run contract migration</span>
psql -h <span class="hljs-variable">$DB_ENDPOINT</span> -U dbadmin -d ecommerce -f /tmp/002_contract_address.sql

<span class="hljs-comment"># Verify old column is gone</span>
psql -h <span class="hljs-variable">$DB_ENDPOINT</span> -U dbadmin -d ecommerce -c <span class="hljs-string">"\d customers"</span>
</code></pre>
<p>The contract migration (<a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs/blob/main/migrations/002_contract_address.sql"><code>migrations/002_contract_address.sql</code></a>) removes the old <code>address</code> column.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768140955991/d6f6f287-09e5-4693-a4e9-77c1d9080466.png" alt="d6f6f287-09e5-4693-a4e9-77c1d9080466" class="image--center mx-auto" width="1506" height="444" loading="lazy"></p>
<p><strong>Why wait 24-72 hours:</strong> You want to be absolutely certain green is stable before making irreversible changes. During this waiting period:</p>
<ul>
<li><p>All your monitoring should show green performing normally</p>
</li>
<li><p>You've seen the system handle multiple daily traffic patterns (morning peak, evening peak, overnight)</p>
</li>
<li><p>Weekly batch jobs have run successfully</p>
</li>
<li><p>You've verified third-party integrations work</p>
</li>
<li><p>No unusual errors or performance degradation</p>
</li>
</ul>
<p>It’s important to snapshot first because once you drop that column, there's no undo button. The snapshot is your safety net. If you discover a critical issue after contracting, you can restore this snapshot and get back to a state where rollback is possible. Without it, you're gambling.</p>
<p><strong>What the contract migration does:</strong></p>
<pre><code class="lang-sql"><span class="hljs-comment">-- migrations/002_contract_address.sql</span>
<span class="hljs-keyword">BEGIN</span>;
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> customers <span class="hljs-keyword">DROP</span> <span class="hljs-keyword">COLUMN</span> address;
<span class="hljs-keyword">COMMIT</span>;
</code></pre>
<p>It's simple but permanent. The old <code>address</code> column is gone. The Blue environment will no longer work with this database, as it expects that column to exist. This is fine because blue has been decommissioned (no traffic, tasks terminated).</p>
<p><strong>What to update:</strong> You should also deploy version 3 of your application that removes the dual-write logic. Version 2 (green) is still writing to both the new columns and the old <code>address</code> column. Version 3 can stop wasting cycles writing to a column that no longer exists.</p>
<p>The contract migration (<a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs/blob/main/migrations/002_contract_address.sql"><code>migrations/002_contract_address.sql</code></a>) removes the old <code>address</code> column. Your migration is now complete!</p>
<h2 id="heading-rollback-strategies">Rollback Strategies</h2>
<h3 id="heading-during-deployment-safe-window">During Deployment (Safe Window)</h3>
<p>Use this strategy when you detect issues <strong>during the traffic shift</strong>, before all traffic has moved to green. CodeDeploy is still managing the deployment, which means it can automatically revert traffic distribution to the previous state.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Immediate rollback</span>
aws deploy stop-deployment \
    --deployment-id <span class="hljs-variable">$DEPLOYMENT_ID</span> \
    --auto-rollback-enabled
</code></pre>
<p>You should use this strategy when you notice increased error rates, degraded performance, or functional issues during the canary or linear traffic shift. CodeDeploy automatically shifts all traffic back to blue, and green tasks are terminated. This is the safest and fastest rollback option.</p>
<p>This works because the database still contains the old <code>address</code> column (expand phase), so blue can function normally. No data has been lost or made incompatible.</p>
<h3 id="heading-after-deployment-before-contract">After Deployment (Before Contract)</h3>
<p>Use this when the deployment completed successfully, but you discover issues hours or days later during the monitoring period, before you've run the contract migration. Both blue and green environments still exist, and the database supports both schemas.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Manual listener update</span>
aws elbv2 modify-listener \
    --listener-arn $(terraform output -raw alb_listener_arn) \
    --default-actions Type=forward,TargetGroupArn=$(terraform output -raw blue_target_group_arn)
</code></pre>
<p>Or use the provided script:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> scripts
./rollback.sh
</code></pre>
<p>Use this when you discover bugs in green that weren't caught during initial testing, business metrics show unexpected changes (conversion rates drop, customer complaints increase), or third-party integration issues emerge.</p>
<p>This works because the database still has both old and new schema elements. Blue tasks still exist and can serve traffic immediately. Because green was writing to both old and new columns, blue sees all the latest data.</p>
<p>With this, the traffic immediately shifts from green back to blue. Green continues running for observability, but serves no traffic. You can debug green in place without customer impact.</p>
<h3 id="heading-after-contract-phase">After Contract Phase</h3>
<p>Use this as a <strong>last resort</strong> when you've already removed the old address column, and blue can no longer function with the current database schema. This is significantly more complex and time-consuming than the previous two strategies.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Restore from snapshot</span>
aws rds restore-db-instance-from-db-snapshot \
    --db-instance-identifier ecommerce-bluegreen-db-restored \
    --db-snapshot-identifier pre-contract-YYYYMMDD-HHMMSS
</code></pre>
<p>Only use this strategy when you discover a critical, production-breaking issue after the contract phase, and you have no other option but to return to the previous version.</p>
<p><strong>Why it's painful</strong>:</p>
<ul>
<li><p>Database restore takes 10-30 minutes depending on size</p>
</li>
<li><p>You lose all data written after the snapshot was taken</p>
</li>
<li><p>Requires updating connection strings to point to the restored instance</p>
</li>
<li><p>Need to re-deploy blue environment</p>
</li>
<li><p>Must communicate downtime to users</p>
</li>
</ul>
<p>This is why you wait 24-72 hours before contracting, and take a snapshot immediately before the contract migration. The lengthy waiting period allows you to catch most issues while the safer rollback strategies are still available.</p>
<h2 id="heading-monitoring-during-deployments">Monitoring During Deployments</h2>
<h3 id="heading-essential-metrics">Essential Metrics</h3>
<p>During a blue-green deployment, you need to monitor both environments simultaneously to detect issues early and make informed decisions about proceeding or rolling back.For each target group (blue and green), track these CloudWatch metrics:</p>
<h4 id="heading-1-targetresponsetime">1. TargetResponseTime</h4>
<p>Measures latency from when the load balancer sends a request to when it receives a response. You're looking for sudden spikes or gradual degradation. Green should have similar response times to blue (within 10-20%). If green's latency is significantly higher, you may have performance regressions, inefficient queries with the new schema, or resource constraints.</p>
<h4 id="heading-2-requestcount">2. RequestCount</h4>
<p>Shows traffic volume hitting each target group. During the deployment, you should see blue's count decreasing while green's increases proportionally. If the numbers don't add up (total requests drop significantly), users might be experiencing errors and not retrying. If green receives traffic but shows zero requests, health checks might be failing.</p>
<h4 id="heading-3-httpcodetarget5xxcount">3. HTTPCode_Target_5XX_Count</h4>
<p>Server errors indicate application problems. Even a single 5XX error during deployment warrants investigation. Green should have zero 5XX errors during the initial traffic shift. Any errors could indicate incompatibility issues with the new schema, missing environment variables, or database connection problems.</p>
<h4 id="heading-4-databaseconnections-from-rds-metrics">4. DatabaseConnections (from RDS metrics):</h4>
<p>Shows active database connections from both environments. Watch for connection pool exhaustion, which manifests as a sudden spike or plateau at your max connections limit. If green uses more connections than blue did, you might have connection leaks or inefficient connection handling in the new code.</p>
<h4 id="heading-5-cpuutilization">5. CPUUtilization</h4>
<p>Monitor both ECS task CPU and RDS CPU. Green tasks should use similar CPU to blue tasks for the same request volume. Higher CPU might indicate less efficient code or more complex queries. RDS CPU spikes during deployment often indicate poorly optimized new queries or missing indexes for the new schema.</p>
<p><strong>What to expect</strong>:</p>
<ul>
<li><p>First 5-10 minutes: Green receives 10% traffic, metrics should closely match blue's baseline</p>
</li>
<li><p>15-20 minutes: Green at 30-50% traffic, both environments should show stable metrics</p>
</li>
<li><p>25-30 minutes: Green at 100% traffic, metrics should stabilize at historical levels</p>
</li>
<li><p>Any divergence from these patterns warrants stopping the deployment and investigating</p>
</li>
</ul>
<p><strong>Custom application metrics</strong>: Beyond infrastructure metrics, monitor business-critical metrics like checkout completion rates, API success rates, and user sign-up flows. Sometimes technical metrics look fine but user-facing functionality is broken.</p>
<h2 id="heading-best-practices">Best Practices</h2>
<h3 id="heading-test-migrations-in-staging">Test Migrations in Staging</h3>
<p>Always run your database migrations against a staging environment that mirrors production scale and complexity before touching production. Copy a recent production snapshot to staging and execute your expand migration there first.</p>
<p><strong>Why this matters</strong>: Migrations that work fine on small datasets can timeout or lock tables on production-scale data. You might discover that adding an index to a 50-million-row table takes 2 hours, or that your column population query needs optimization.</p>
<p><strong>What to test</strong>:</p>
<ul>
<li><p>Migration execution time (should complete in seconds/minutes, not hours)</p>
</li>
<li><p>Table locks and their impact (can reads/writes continue during migration?)</p>
</li>
<li><p>Query performance with new schema (are your indexes still effective?)</p>
</li>
<li><p>Rollback procedures (can you undo the migration if needed?)</p>
</li>
</ul>
<h3 id="heading-use-migration-tools">Use Migration Tools</h3>
<p>Don't write raw SQL migrations manually. Use Flyway, Liquibase, Alembic (for Python), or your framework's built-in migration tools (Rails migrations, Django migrations, Entity Framework migrations).</p>
<p><strong>Why this matters</strong>: Migration tools provide version tracking, rollback capabilities, checksums to prevent tampering, and a standardized way to manage schema changes across environments.</p>
<h3 id="heading-configure-health-checks-properly">Configure Health Checks Properly</h3>
<p>Your health check endpoint should verify that the application can actually function, not just that the process is running. A comprehensive health check validates database connectivity, schema compatibility, and dependent service availability.</p>
<pre><code class="lang-python"><span class="hljs-meta">@app.route('/health')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">health_check</span>():</span>
    checks = {
        <span class="hljs-string">'database'</span>: check_database(),
        <span class="hljs-string">'schema'</span>: check_schema_compatibility(),
        <span class="hljs-string">'cache'</span>: check_cache_connection()
    }

    <span class="hljs-keyword">if</span> all(checks.values()):
        <span class="hljs-keyword">return</span> jsonify(checks), <span class="hljs-number">200</span>
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">return</span> jsonify(checks), <span class="hljs-number">503</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">check_schema_compatibility</span>():</span>
    <span class="hljs-string">"""Verify expected schema elements exist"""</span>
    <span class="hljs-keyword">try</span>:
        result = db.query(<span class="hljs-string">"""
            SELECT column_name 
            FROM information_schema.columns 
            WHERE table_name = 'customers'
            AND column_name IN ('street_address', 'city', 'state', 'zip_code')
        """</span>)
        <span class="hljs-keyword">return</span> len(result) == <span class="hljs-number">4</span>
    <span class="hljs-keyword">except</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
</code></pre>
<p>For ALB health checks specifically, make sure you configure appropriate thresholds in your target group settings. A healthy threshold of 2 means the target must pass 2 consecutive health checks before receiving traffic. An unhealthy threshold of 3 means it must fail 3 consecutive checks before being removed. Set your interval to 30 seconds and timeout to 5 seconds to balance responsiveness with stability.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Terraform configuration for ALB health checks</span>
resource <span class="hljs-string">"aws_lb_target_group"</span> <span class="hljs-string">"green"</span> {
  health_check {
    enabled             = <span class="hljs-literal">true</span>
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    path                = <span class="hljs-string">"/health"</span>
    matcher             = <span class="hljs-string">"200"</span>
  }
}
</code></pre>
<p>This configuration ensures that ECS tasks aren't marked healthy prematurely (preventing traffic to broken tasks) while also not being overly sensitive to transient issues (preventing unnecessary task replacements).</p>
<h3 id="heading-plan-the-contract-phase">Plan the Contract Phase</h3>
<p>The contract phase is irreversible, so treat it with appropriate caution. Wait a minimum of 24-72 hours after green deployment before removing old schema elements. This waiting period isn't arbitrary: it ensures you've observed the system under various conditions.</p>
<p><strong>What to verify before contracting</strong>:</p>
<ul>
<li><p>Green has handled multiple daily traffic patterns (morning rush, evening peak, overnight batch jobs)</p>
</li>
<li><p>All scheduled jobs and cron tasks have run successfully with the new schema</p>
</li>
<li><p>Weekly reports or analytics pipelines have completed</p>
</li>
<li><p>Third-party integrations (payment processors, shipping APIs, analytics tools) are working</p>
</li>
<li><p>No unusual error patterns in logs</p>
</li>
<li><p>Business metrics (conversions, sign-ups, purchases) remain stable</p>
</li>
<li><p>Customer support hasn't reported related issues</p>
</li>
</ul>
<p>The pre-contract checklist:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. Create a final snapshot</span>
aws rds create-db-snapshot \
    --db-instance-identifier ecommerce-bluegreen-db \
    --db-snapshot-identifier pre-contract-$(date +%Y%m%d-%H%M%S)

<span class="hljs-comment"># 2. Document current state</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Green tasks: <span class="hljs-subst">$(aws ecs describe-services --cluster ecommerce --services ecommerce-green | jq '.services[0].runningCount')</span>"</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Error rate: <span class="hljs-subst">$(aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB --metric-name HTTPCode_Target_5XX_Count --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S)</span> --end-time <span class="hljs-subst">$(date -u +%Y-%m-%dT%H:%M:%S)</span> --period 3600 --statistics Sum)"</span>

<span class="hljs-comment"># 3. Notify team</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Running contract migration at <span class="hljs-subst">$(date)</span>"</span>

<span class="hljs-comment"># 4. Run migration</span>
psql -h <span class="hljs-variable">$DB_ENDPOINT</span> -U dbadmin -d ecommerce -f migrations/002_contract_address.sql

<span class="hljs-comment"># 5. Verify</span>
psql -h <span class="hljs-variable">$DB_ENDPOINT</span> -U dbadmin -d ecommerce -c <span class="hljs-string">"\d customers"</span>
</code></pre>
<h3 id="heading-version-your-apis">Version Your APIs</h3>
<p>When changing data formats, maintain backward compatibility by supporting both old and new API versions simultaneously. This allows API consumers (mobile apps, third-party integrations, other services) to migrate at their own pace without coordinating releases.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Support both API versions during transition</span>
<span class="hljs-meta">@app.route('/api/v1/customers/&lt;id&gt;')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_customer_v1</span>(<span class="hljs-params">id</span>):</span>
    customer = Customer.find(id)
    <span class="hljs-keyword">return</span> jsonify({
        <span class="hljs-string">'id'</span>: customer.id,
        <span class="hljs-string">'name'</span>: customer.name,
        <span class="hljs-string">'address'</span>: customer.address  <span class="hljs-comment"># Old format</span>
    })

<span class="hljs-meta">@app.route('/api/v2/customers/&lt;id&gt;')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_customer_v2</span>(<span class="hljs-params">id</span>):</span>
    customer = Customer.find(id)
    <span class="hljs-keyword">return</span> jsonify({
        <span class="hljs-string">'id'</span>: customer.id,
        <span class="hljs-string">'name'</span>: customer.name,
        <span class="hljs-string">'address'</span>: {  <span class="hljs-comment"># New structured format</span>
            <span class="hljs-string">'street'</span>: customer.street_address,
            <span class="hljs-string">'city'</span>: customer.city,
            <span class="hljs-string">'state'</span>: customer.state,
            <span class="hljs-string">'zip'</span>: customer.zip_code
        }
    })
</code></pre>
<p>To implement this, you can initially deploy both endpoints with blue-green. Then monitor usage of v1 endpoint over time. Once v1 traffic drops below 1% (meaning clients have migrated), deprecate it formally. Remove v1 endpoint in a subsequent release, not during the blue-green deployment itself.</p>
<p>Announce the new API version to consumers with a migration timeline. Give them 2-3 months to update their integrations. Send reminder emails at the halfway point and 2 weeks before v1 shutdown.</p>
<h3 id="heading-monitor-both-environments">Monitor Both Environments</h3>
<p>During the transition period, both blue and green are production environments serving real traffic. Monitor them separately to detect version-specific issues.</p>
<p>Set up separate CloudWatch dashboards for blue and green target groups with the same metrics arranged identically. This makes it easy to spot differences at a glance. If green's response time is 200ms while blue's is 50ms, that's a red flag.</p>
<h4 id="heading-alert-on-metric-divergence">Alert on metric divergence</h4>
<p>Create alarms that trigger when green's metrics deviate significantly from blue's baseline. For example, if green's error rate is more than 2x blue's historical average, trigger an alert. If green's database query time is 50% higher, investigate before shifting more traffic.</p>
<h4 id="heading-log-aggregation">Log aggregation</h4>
<p>Ensure logs from both environments are tagged with their version (<code>environment: blue</code> or <code>environment: green</code>) so you can filter and compare them. Use CloudWatch Insights queries to spot patterns.</p>
<h2 id="heading-when-not-to-use-blue-green">When NOT to Use Blue-Green</h2>
<p>Blue-green isn't always the right choice. Avoid it when you have:</p>
<ul>
<li><p><strong>Very large database migrations</strong>: If your migration takes hours or requires significant locks, use a traditional maintenance window.</p>
</li>
<li><p><strong>Highly stateful applications</strong>: Real-time collaboration tools or WebSocket applications with complex in-memory state may need rolling deployments instead.</p>
</li>
<li><p><strong>Cost constraints</strong>: Running two environments doubles costs. Consider canary deployments for cost-sensitive applications.</p>
</li>
<li><p><strong>Complex data model redesigns</strong>: Use the strangler fig pattern to gradually migrate functionality to a new service.</p>
</li>
</ul>
<h3 id="heading-alternative-deployment-strategies">Alternative Deployment Strategies</h3>
<h4 id="heading-canary-deployments">Canary Deployments</h4>
<p>Route a small percentage (5-10%) to the new version:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"trafficRouting"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TimeBasedCanary"</span>,
    <span class="hljs-attr">"timeBasedCanary"</span>: {
      <span class="hljs-attr">"canaryPercentage"</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">"canaryInterval"</span>: <span class="hljs-number">5</span>
    }
  }
}
</code></pre>
<h3 id="heading-rolling-deployments">Rolling Deployments</h3>
<p>Gradually replace old tasks with new ones:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"deploymentConfiguration"</span>: {
    <span class="hljs-attr">"maximumPercent"</span>: <span class="hljs-number">200</span>,
    <span class="hljs-attr">"minimumHealthyPercent"</span>: <span class="hljs-number">100</span>
  }
}
</code></pre>
<h2 id="heading-cleanup">Cleanup</h2>
<p>After you've successfully completed your blue-green deployment, validated the green environment, and run the contract phase, you need to clean up the AWS resources to avoid unnecessary costs and resource sprawl.</p>
<p><strong>What you're removing</strong>:</p>
<ul>
<li><p>The entire infrastructure stack (VPC, subnets, NAT gateways, load balancer, ECS cluster, RDS database, and all associated resources)</p>
</li>
<li><p>This is appropriate for a tutorial/testing scenario where you deployed everything from scratch</p>
</li>
</ul>
<p>Important considerations before cleanup:</p>
<ul>
<li><p>Ensure you have backups if you need to reference any data later</p>
</li>
<li><p>Export any logs or metrics you want to retain</p>
</li>
<li><p>Document lessons learned from the deployment</p>
</li>
<li><p>Verify no production traffic is still using these resources</p>
</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> terraform

<span class="hljs-comment"># Terraform will prompt you to confirm with "yes"</span>
<span class="hljs-comment"># Review the destruction plan carefully before confirming</span>
terraform destroy  <span class="hljs-comment"># Takes ~10-15 minutes</span>
</code></pre>
<p><strong>Partial cleanup</strong>: If you want to keep certain resources (like RDS snapshots for reference), you can remove them from Terraform state before destroying:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Remove RDS from Terraform management before destroying</span>
terraform state rm aws_db_instance.main
terraform destroy  <span class="hljs-comment"># Now destroys everything except RDS</span>
</code></pre>
<p>For production environments, you would NOT destroy everything. Instead, you'd decommission the blue environment specifically after confirming green is stable:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Production scenario - remove only blue environment</span>
terraform destroy -target=aws_ecs_service.blue
terraform destroy -target=aws_lb_target_group.blue
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Blue-green deployments with databases require careful planning, but the expand-contract pattern makes it manageable.</p>
<p>Here are some key takeaways:</p>
<ol>
<li><p><strong>Use expand-contract as default</strong> – Maintains backwards compatibility and safe rollbacks.</p>
</li>
<li><p><strong>Externalize state</strong> – Sessions, caches, and storage should use external services.</p>
</li>
<li><p><strong>Plan for three phases</strong> – Don't rush to the contract phase.</p>
</li>
<li><p><strong>Test everything in staging</strong> – Mirror production scale and complexity.</p>
</li>
<li><p><strong>Monitor aggressively</strong> – Track technical and business metrics for both environments.</p>
</li>
<li><p><strong>Know when to use alternatives</strong> – Blue-green isn't always the answer.</p>
</li>
<li><p><strong>Document rollback procedures</strong> – Everyone should know the rollback process before deployment.</p>
</li>
</ol>
<p>The expand-contract pattern requires more work upfront, but this investment pays dividends in reduced risk and maintained uptime. With the strategies and complete implementation provided here, you can successfully deploy even complex, stateful applications with confidence.</p>
<p>As always, I hope you enjoyed this guide and learned something. If you want to stay connected or see more hands-on DevOps content, you can follow me on <a target="_blank" href="https://www.linkedin.com/in/destiny-erhabor">LinkedIn</a>.</p>
<p>For more practical hands-on Cloud/DevOps projects like this one, follow and star this repository: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building">Learn-DevOps-by-building</a>.</p>
<h2 id="heading-further-resources">Further Resources</h2>
<ul>
<li><p>Complete Code: <a target="_blank" href="https://github.com/Caesarsage/bluegreen-deployment-ecs">github.com/Caesarsage/bluegreen-deployment-ecs</a></p>
</li>
<li><p>Learn DevOps by Building: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building">GitHub repo</a></p>
</li>
<li><p>AWS ECS Blue/Green Documentation: <a target="_blank" href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-bluegreen.html">AWS Docs</a></p>
</li>
<li><p>AWS CodeDeploy for ECS: <a target="_blank" href="https://docs.aws.amazon.com/codedeploy/latest/userguide/deployment-steps-ecs.html">AWS Docs</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Your AI Demos with Gradio ]]>
                </title>
                <description>
                    <![CDATA[ The world of artificial intelligence moves fast. Every week, new models appear, older ones get better, and the tools to use them become easier. But if you are building a machine learning project, you may face one big problem: how to share your work q... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-your-ai-demos-with-gradio/</link>
                <guid isPermaLink="false">68afa02d9547303a1dcc65cf</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Thu, 28 Aug 2025 00:17:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1756337736951/2a58e22d-dd7a-4768-b052-a981a91c36da.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The world of artificial intelligence moves fast. Every week, new models appear, older ones get better, and the tools to use them become easier.</p>
<p>But if you are building a machine learning project, you may face one big problem: how to share your work quickly so that others can try it.</p>
<p>A notebook full of code is not always enough. People want to interact with your model. They want to see inputs, click buttons, and watch results appear instantly.</p>
<p>This is where <a target="_blank" href="https://www.gradio.app/"><strong>Gradio</strong></a> comes in. With just a few lines of Python, you can turn your AI model into a simple web app. You don’t need to know HTML, CSS, or JavaScript, Gradio takes care of the interface so you can focus on your model.</p>
<p>In this tutorial, you will learn how to build AI demos in minutes using Gradio. By the end, you will have a live demo ready for anyone to test.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-gradio">What is Gradio?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-use-gradio">Why Use Gradio?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-your-first-gradio-app">Your First Gradio App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-add-machine-learning-models">How to Add Machine Learning Models</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-customize-the-interface">How to Customize the Interface</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-share-your-app">How to Share Your App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-gradio">What is Gradio?</h2>
<p>Gradio is an open-source Python library that makes it easy to create interactive web interfaces for machine learning models. </p>
<p>Imagine that you trained a text summarizer or an image classifier. Without Gradio, you would have to build a frontend, write backend code, host it somewhere, and then connect it all together. That takes time and effort.</p>
<p>With Gradio, you write a few lines of Python, and it gives you a shareable link with a complete UI. The interface works on any device with a browser. You can even embed it in websites or share it with teammates for feedback.</p>
<p>Gradio supports text, images, audio, video, and many other data types. This makes it perfect for computer vision, natural language processing, speech recognition, or any other AI application.</p>
<h2 id="heading-why-use-gradio">Why Use Gradio?</h2>
<p>Speed is a major reason for choosing Gradio. Building a web app for your model can take hours or even days if you do it from scratch. Gradio reduces that to minutes. You focus on your AI model while Gradio handles the user interface.</p>
<p>It is also easy to use. Even beginners with basic Python knowledge can create functional demos. It works well with popular libraries like TensorFlow, PyTorch, and <a target="_blank" href="https://www.turingtalks.ai/p/hugging-face-s-transformer-library-a-game-changer-in-nlp">Hugging Face Transformers</a>.</p>
<p>Another advantage is sharing. When you launch a Gradio app, you get a public link that anyone can open. You don’t need to deploy it manually or set up servers. This makes it perfect for hackathons, quick prototypes, or sending demos to clients and friends.</p>
<h4 id="heading-how-to-install-gradio">How to Install Gradio</h4>
<p>Before building your first app, you need to install Gradio. Open your terminal or command prompt and type:</p>
<pre><code class="lang-plaintext">pip install gradio
</code></pre>
<p>That’s it. The installation is quick and usually takes less than a minute. Once done, you are ready to build your first demo.</p>
<h2 id="heading-your-first-gradio-app">Your First Gradio App</h2>
<p>Let’s start simple. Imagine you want to build a text reversal app. The user types a sentence, and the app shows the reversed version. It may not be a real AI model, but it helps you learn the basics.</p>
<p>Here’s the code:</p>
<pre><code class="lang-plaintext"># Import the Gradio library
import gradio as gr

# Define a function that reverses any input text
def reverse_text(text):
    # The [::-1] slice notation reverses the string
    return text[::-1]

# Create a Gradio interface to connect the function with a simple web UI
demo = gr.Interface(
    fn=reverse_text,       # Function to call when the user submits input
    inputs="text",         # Type of input (a text box for user input)
    outputs="text",        # Type of output (a text box to display reversed text)
    title="Text Reversal App",          # Title displayed on the app
    description="Type any text and see it reversed instantly."  # Short description for users
)

# Launch the web app in the browser
demo.launch()
</code></pre>
<p><code>gr.Interface()</code> links your Python function to a web-based user interface. <code>fn=reverse_text</code> tells Gradio to call this function whenever the user provides input.</p>
<p><code>inputs="text"</code> specifies that the input field should be a text box. <code>outputs="text"</code> makes the output display as text.</p>
<p><code>title</code> and <code>description</code> improve the look of the app with a heading and explanation.</p>
<p>Save this in a Python file and run it. A browser window will open with a text box. Type something, hit submit, and you will see the reversed text appear.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756100382572/5fdc0ace-b9ab-43a5-943b-1174670e7cd1.png" alt="Gradio Result" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Congratulations! You just built your first interactive app with Gradio in under five minutes. </p>
<h2 id="heading-how-to-add-machine-learning-models">How to Add Machine Learning Models</h2>
<p>Now let’s build something more exciting. Suppose you have a <a target="_blank" href="https://www.freecodecamp.org/news/how-to-build-a-simple-sentiment-analyzer-using-hugging-face-transformer/">sentiment analysis</a> model that takes text and predicts whether it is positive, negative, or neutral. You can connect it to Gradio easily.</p>
<p>Here is an example using Hugging Face Transformers:</p>
<pre><code class="lang-plaintext"># Import the Gradio library
import gradio as gr

# Import the 'pipeline' function from Hugging Face's Transformers library
# 'pipeline' lets you load pre-trained AI models with a single line of code
from transformers import pipeline

# Load a pre-trained sentiment analysis model from Hugging Face
# This model can classify text as POSITIVE, NEGATIVE, or NEUTRAL along with a confidence score
sentiment_model = pipeline("sentiment-analysis")

# Define a function that uses the model to analyze text sentiment
def analyze_sentiment(text):
    # Pass the user-provided text to the model
    # The model returns a list of predictions; we take the first one using [0]
    result = sentiment_model(text)[0]

    # Return the label (e.g., POSITIVE) and the confidence score formatted to 2 decimal places
    return f"Label: {result['label']}, Score: {result['score']:.2f}"

# Create a Gradio interface to turn the function into a web app
demo = gr.Interface(
    fn=analyze_sentiment,         # The function to call when user inputs text
    inputs="text",                # The input type (a single-line text box)
    outputs="text",               # The output type (display as text)
    title="Sentiment Analysis App",    # Title shown at the top of the web app
    description="Type a sentence to check its sentiment."  # Short explanation for the app
)

# Launch the web app so users can interact with it in a browser
demo.launch()
</code></pre>
<p>Run this code, type “I love this product!” and watch the model return “Label: POSITIVE” with a confidence score. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756100423148/15899f62-f962-488e-8dba-df9809ad56c1.png" alt="15899f62-f962-488e-8dba-df9809ad56c1" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-customize-the-interface">How to Customize the Interface</h2>
<p>Gradio gives you control over titles, descriptions, themes, and even examples. For example, you can add example inputs like this:</p>
<pre><code class="lang-plaintext">demo = gr.Interface(fn=analyze_sentiment, 
                    inputs="text", 
                    outputs="text",
                    title="Sentiment Analysis App",
                    description="Type a sentence to check its sentiment.",
                    examples=[["I love AI"], ["I hate waiting"]])
</code></pre>
<p>Now the app shows example sentences that users can click to test instantly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756100452336/6f8ee1d4-bcb1-4719-bbeb-74401f7d8990.png" alt="Gradio demo with examples" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-share-your-app">How to Share Your App</h2>
<p>When you run <code>demo.launch()</code>, Gradio starts a local server and gives you a local link. To get a sharable link, use <code>demo.launch(share=True)</code> and you will get a public link that you can share with others. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756100477123/2b5fbb37-cbda-46e4-be5a-18422b9c6f78.png" alt="Public url for sharing demos" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The public link works for 72 hours by default. If you want a permanent link, you can deploy on Hugging Face Spaces for free or use platforms like AWS. </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Gradio changes how developers share machine learning models. What once took hours of coding now takes minutes. You write the model code, connect it to Gradio, and instantly get a working demo with a shareable link.</p>
<p>Whether you are a student learning AI, a researcher sharing results, or a developer building prototypes, Gradio saves you time and effort. It removes the complexity of web development so you can focus on what matters: building your AI model.</p>
<p><em>I Hope you enjoyed this article. Signup for my free AI newsletter</em> <a target="_blank" href="https://www.turingtalks.ai/"><strong><em>TuringTalks.ai</em></strong></a> <em>for more hands-on tutorials on AI. You can also find me on</em> <a target="_blank" href="https://www.linkedin.com/in/manishmshiva"><strong><em>Linkedin</em></strong></a><strong><em>.</em></strong></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy a Kubernetes App on AWS EKS ]]>
                </title>
                <description>
                    <![CDATA[ AWS makes it much easier to deploy containerized applications, and running Kubernetes in the cloud is a powerful way to scale and manage these applications. Among the many managed Kubernetes services AWS offers, Amazon EKS (Elastic Kubernetes Service... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-deploy-a-kubernetes-app-on-aws-eks/</link>
                <guid isPermaLink="false">68a85abd010317ca95b5506e</guid>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ijeoma Igboagu ]]>
                </dc:creator>
                <pubDate>Fri, 22 Aug 2025 11:55:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755863277691/28937c4d-5862-464d-84a1-dfab01a577bb.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>AWS makes it much easier to deploy containerized applications, and running Kubernetes in the cloud is a powerful way to scale and manage these applications.</p>
<p>Among the many managed Kubernetes services AWS offers, Amazon EKS (Elastic Kubernetes Service) stands out for its seamless integration with the AWS ecosystem, strong reliability, and excellent support.</p>
<p>If you’re ready to move beyond local setups and want to deploy a real-world Kubernetes app on AWS EKS, this guide will walk you through the entire process. Whether you’re working on a microservice, a full-stack app, or just experimenting with Kubernetes in an environment that mimics production, you’ll find this walkthrough useful.</p>
<p>In this article, I’ll guide you through the process of creating your EKS cluster, deploying your application, and making it accessible over the internet, step by step.</p>
<h2 id="heading-what-well-cover">What We’ll Cover:</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-a-kubernetes-cluster">What is a Kubernetes Cluster?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-amazon-elastic-kubernetes-service">What is Amazon Elastic Kubernetes Service?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-use-amazon-eks-for-kubernetes">Why Use Amazon EKS for Kubernetes?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-a-kubernetes-cluster-using-aws">How to Create a Kubernetes Cluster Using AWS</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get started, make sure you have the following installed on your local machine:</p>
<ul>
<li><p>Have a basic understanding of cloud services.</p>
</li>
<li><p>Have a basic understanding of the Linux command line.</p>
</li>
<li><p>Have an <a target="_blank" href="https://aws.amazon.com/free/">AWS account</a>.</p>
</li>
<li><p>Install eksctl, a simple CLI tool to create and manage EKS clusters.</p>
</li>
<li><p>Install kubectl, the standard Kubernetes command-line tool.</p>
</li>
<li><p>Install Docker to build and package your app into a container.</p>
</li>
</ul>
<p>Before setting up a Kubernetes cluster for our application, it’s essential to understand a few basic concepts.</p>
<h2 id="heading-what-is-a-kubernetes-cluster"><strong>What is a Kubernetes Cluster?</strong></h2>
<p>A Kubernetes (also called K8S) cluster consists of machines (called nodes) that run containerized applications. It works alongside container engines like <a target="_blank" href="https://cri-o.io/#what-is-cri-o"><strong>CRI-O</strong></a> or <a target="_blank" href="https://containerd.io/"><strong>containerd</strong></a> to help you deploy and manage your apps more efficiently.</p>
<p>Kubernetes nodes come in two main types:</p>
<ul>
<li><p><strong>Master nodes (control plane):</strong> These handle the brainwork, such as scheduling, scaling, and managing the cluster’s overall state.</p>
</li>
<li><p><strong>Worker nodes (data plane):</strong> They run the actual applications inside the containers.</p>
</li>
</ul>
<p>If you're new to Kubernetes or want to brush up, check out the free course <a target="_blank" href="https://training.linuxfoundation.org/training/introduction-to-kubernetes/?lid=axmt8lvbjbl8"><strong>Introduction to Kubernetes (LFS158)</strong></a> from the Linux Foundation.</p>
<h2 id="heading-what-is-amazon-elastic-kubernetes-service">What is Amazon Elastic Kubernetes Service?</h2>
<p>Amazon Elastic Kubernetes Service (EKS) is a managed service that enables easy Kubernetes deployment on AWS, eliminating the need to set up and maintain your own Kubernetes control plane node.</p>
<p>AWS EKS takes care of the heavy lifting by managing the control plane, handling upgrades, and installing core components, such as the container runtime and essential Kubernetes processes. It also offers built-in tools for scaling, high availability, and backup.</p>
<p>With EKS, you or your team can focus on building and running applications, while AWS handles the underlying infrastructure.</p>
<h2 id="heading-why-use-amazon-eks-for-kubernetes"><strong>Why Use Amazon EKS for Kubernetes?</strong></h2>
<p>Here are some key benefits of using AWS EKS:</p>
<ul>
<li><p>EKS handles upgrades, patching, and high availability for you, giving you a fully managed control plane with minimal manual effort.</p>
</li>
<li><p>You can easily scale your applications, and the infrastructure grows as your needs evolve.</p>
</li>
<li><p>It has built-in support for IAM roles, private networking, and encryption.</p>
</li>
<li><p>AWS EKS runs on highly available infrastructure across multiple AWS Availability Zones, making your application available globally.</p>
</li>
<li><p>With Amazon EKS, you get the power of Kubernetes without managing the underlying setup. So you can stay focused on building and running your apps.</p>
</li>
</ul>
<h2 id="heading-how-to-create-a-kubernetes-cluster-using-aws"><strong>How to Create a Kubernetes Cluster Using AWS</strong></h2>
<p>Now let’s walk through the process of getting a Kubernetes cluster up and running.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754818871609/8b0f622a-af82-4a29-bb22-fbdb1a6279bb.png" alt="lets get started" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-1-how-to-install-the-tools-needed-to-create-a-cluster"><strong>Step 1: How to Install the Tools Needed to Create a Cluster</strong></h3>
<p>The easiest and most developer-friendly way to spin up an Elastic Kubernetes Service that you can use at the production level is by using <strong>eksctl</strong>. It takes care of the manual setup and automatically provisions the necessary AWS resources.</p>
<p>Before we begin, we need to install two essential tools:</p>
<ul>
<li><p><strong>eksctl</strong> – This is used to create and manage your EKS cluster.</p>
</li>
<li><p><strong>kubectl</strong> – This allows you to interact with your cluster, deploy apps, and manage Kubernetes resources.</p>
</li>
</ul>
<p>These tools will make it easy to set up your Kubernetes cluster and work with it directly from your terminal.</p>
<h4 id="heading-how-to-install-eksctl">How to Install eksctl</h4>
<p>Open your browser and go to the official <a target="_blank" href="https://eksctl.io/">eksctl</a> documentation. Scroll down to the <strong>Installation</strong> section.</p>
<p>Scroll to the <strong>Unix</strong> instructions if you're using Ubuntu or a similar system. Then copy the installation command and paste it into your terminal.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754819104540/48310ddc-89fb-49b9-990b-ca425ac81e55.gif" alt="Installing eksctl tool" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Once it’s done, run <code>eksctl version</code> to confirm that the installation was successful.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754819269922/69d18189-f3ea-421b-9666-e37c43d7c077.gif" alt="checking the version" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h4 id="heading-how-to-install-kubectl">How to Install kubectl</h4>
<p>The next step is to install <a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/">kubectl</a>. You can find the installation instructions in the official Kubernetes documentation, which provides steps based on your operating system.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754819717619/028418cb-424a-4514-a7a7-dfd17c2c03ce.gif" alt="Installing kubectl" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-2-how-to-create-the-elastic-kubernetes-service-eks-cluster"><strong>Step 2: How to Create the Elastic Kubernetes Service (EKS) Cluster</strong></h3>
<p>Now that you've installed the tools needed to create and interact with a Kubernetes cluster on AWS, it's time to launch the cluster.</p>
<p>To get started, open your terminal and run the following command:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create an EKS cluster named "k8s-example" in eu-west-2 (London)</span>
eksctl create cluster --name k8s-example --region eu-west-2
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754820013644/a763f4f1-4b5f-47c2-9c09-3148cb77f579.gif" alt="Creating a terminal in your cluster" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>One great thing about using AWS EKS is that once your Kubernetes cluster is created, it automatically updates your <code>~/.kube/config</code> file. This means you can start interacting with your cluster right away, using <code>kubectl</code> – no extra setup needed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754825286662/7b7f68a9-72c1-4306-bc09-a0d5c528b900.png" alt="Cluster ready on AWS" width="600" height="400" loading="lazy"></p>
<p>After running the command (as shown in the GIF above), your Kubernetes cluster is successfully created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754825194425/a6c611d4-837f-4106-955f-99bdcbb9cf5d.gif" alt="Kubernetes cluster created" width="600" height="400" loading="lazy"></p>
<p>Head over to the AWS console, and you’ll see your new cluster listed with a status of <strong>Active</strong>.</p>
<p>With your cluster up and running, it’s time to test the connection. You can do this by running a few <code>kubectl</code> commands in your terminal to list the nodes, pods and namespaces in your cluster.</p>
<p><strong>To test the connection:</strong></p>
<pre><code class="lang-bash">kubectl get nodes
</code></pre>
<p>This command lists all the nodes in your cluster.</p>
<pre><code class="lang-bash">kubectl get pods
</code></pre>
<p>This command lists all the pods currently running.</p>
<pre><code class="lang-bash">kubectl get namespaces
</code></pre>
<p>This command lists all the namespaces currently running.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754825006032/4d2e72ff-8eb9-48be-bd90-efffbb840241.png" alt="testing the cluster" width="600" height="400" loading="lazy"></p>
<p>If each command returns a list of resources, congratulations! Your connection to the Kubernetes cluster is successful.</p>
<h3 id="heading-step-3-how-to-create-kubernetes-manifests"><strong>Step 3: How to Create Kubernetes Manifests</strong></h3>
<p>Let’s define the application using a YAML file. In this file, you’ll create two key resources: a <strong>Deployment</strong> and a <strong>Service</strong>.</p>
<ul>
<li><p>The <strong>Deployment</strong> ensures your application runs reliably by specifying how many replicas to run, which container image to use, and how to manage updates.</p>
</li>
<li><p>The <strong>Service</strong> makes your application accessible — both within the Kubernetes cluster and, if needed, from the internet, even if the underlying pods change or restart.</p>
</li>
</ul>
<p>Together, these resources orchestrate your application so it can run consistently in different environments and be accessed by others.</p>
<pre><code class="lang-yaml"><span class="hljs-comment">#deployment-example.yaml</span>

<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">amazon-deployment</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">amazon-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">5</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">amazon-app</span>
      <span class="hljs-attr">tier:</span> <span class="hljs-string">frontend</span>
      <span class="hljs-attr">version:</span> <span class="hljs-number">1.0</span><span class="hljs-number">.0</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">amazon-app</span>
        <span class="hljs-attr">tier:</span> <span class="hljs-string">frontend</span>
        <span class="hljs-attr">version:</span> <span class="hljs-number">1.0</span><span class="hljs-number">.0</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">amazon-container</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">ooghenekaro/amazon:2</span>
          <span class="hljs-attr">ports:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">3000</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">amazon-service</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">amazon-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">LoadBalancer</span>
  <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
      <span class="hljs-attr">targetPort:</span> <span class="hljs-number">3000</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">amazon-app</span>
</code></pre>
<p>The service uses a <strong>LoadBalancer type,</strong> which tells AWS to provision an <strong>Elastic Load Balancer</strong> (ELB) and route traffic to the pods.</p>
<h3 id="heading-step-4-how-to-deploy-the-app-to-eks"><strong>Step 4: How to Deploy the App to EKS</strong></h3>
<p>Now that your YAML file is defined and the Kubernetes cluster on AWS EKS is ready, it’s time to deploy your application. </p>
<p>To do this, run the following command in your terminal to apply the configuration defined in your manifest file: </p>
<pre><code class="lang-bash">kubectl apply -f deployment-example.yaml
</code></pre>
<p>This command tells Kubernetes to create the necessary pods and services based on what’s specified in the manifest file.</p>
<p>Next, you can check the status of your pods and services:</p>
<pre><code class="lang-bash">kubectl get pods
kubectl get svc or service
kubectl get all
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754824866347/8b48e580-d25f-4965-9c29-3609778365f0.png" alt="checking to see if our application is deployed properly" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-5-how-to-access-your-application"><strong>Step 5: How to Access Your Application</strong></h3>
<p>To view your application in the browser, run the following command to list your services:</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">svc</span>
</code></pre>
<p>Look for the <strong>EXTERNAL-IP</strong> of your service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754824764125/b78b8feb-e5da-4d3c-b467-6ccb88d00373.png" alt="Display of the External IP on the browser" width="600" height="400" loading="lazy"></p>
<p>Copy the IP address and paste it into your browser. Your app should now be live!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754824488894/821522c0-186f-48fd-8c64-ed29a2ba4447.png" alt="live site" width="600" height="400" loading="lazy"></p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Deploying a Kubernetes app on AWS EKS may seem complex at first, but with tools like eksctl and kubectl, the process is surprisingly approachable.</p>
<p>Whether you're a developer experimenting with Kubernetes or a team looking to scale production workloads, EKS provides a strong, scalable foundation that supports your applications as they grow.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://labs.play-with-k8s.com/">Play with Kubernetes</a></p>
</li>
<li><p><a target="_blank" href="https://www.docker.com/101-tutorial/">Docker 101 Tutorial</a></p>
</li>
<li><p><a target="_blank" href="https://dev.to/ijay/how-to-create-a-cicd-using-aws-elastic-beanstalk-15nh">How to Create a CI/CD Pipeline Using AWS Elastic Beanstalk</a></p>
</li>
</ul>
<p>Thanks for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy a Next.js API to Production using Sevalla ]]>
                </title>
                <description>
                    <![CDATA[ When people hear about Next.js, they often think of server-side rendering, React-powered frontends, or SEO-optimised static websites. But there's more to this powerful framework than just front-end development. Next.js also allows developers to build... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-deploy-a-nextjs-api-to-production-using-sevalla/</link>
                <guid isPermaLink="false">688d4a71e3bdfbcce11ae186</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 01 Aug 2025 23:14:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754090039728/75b0f680-94b4-4310-9c52-109000acde22.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When people hear about Next.js, they often think of <a target="_blank" href="https://www.freecodecamp.org/news/server-side-rendering-javascript/">server-side rendering</a>, React-powered frontends, or SEO-optimised static websites. But there's more to this powerful framework than just front-end development.</p>
<p>Next.js also allows developers to build robust, scalable backend APIs directly inside the same codebase. This is especially valuable for small to mid-sized applications where having a tightly coupled frontend and backend speeds up development and deployment.</p>
<p>In this article, you’ll learn how to build an API using Next.js and deploy it to production using Sevalla. It’s relatively easy to learn how to build something using a tutorial – but the real challenge is to get it into the hands of users. Doing so transforms your project from a local prototype into something real and usable.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-nextjs">What is Next.js?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-installation-amp-setup">Installation &amp; Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-a-rest-api">How to Build a REST API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-test-the-api">How to Test the API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-deploy-to-sevalla">How to Deploy to Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-nextjs"><strong>What is Next.js?</strong></h2>
<p><a target="_blank" href="https://nextjs.org/">Next.js</a> is an open-source React framework built by Vercel. It enables developers to build server-rendered and statically generated web applications.</p>
<p>It essentially abstracts the configuration and boilerplate needed to run a full-stack React application, making it easier for developers to focus on building features rather than setting up infrastructure.</p>
<p>While it started as a solution for frontend challenges in React, it has evolved into a full-stack framework that lets you handle backend logic, interact with databases, and build APIs. This unified codebase is what makes Next.js particularly compelling for modern web development.</p>
<h2 id="heading-installation-amp-setup">Installation &amp; Setup</h2>
<p>Let’s install Next.js. Make sure you have <a target="_blank" href="https://nodejs.org/en">Node.js</a> and NPM installed on your system, and that they’re the latest version.</p>
<pre><code class="lang-plaintext">$ node --version
v22.16.0

$ npm --version
10.9.2
</code></pre>
<p>Now let’s create a Next.js project. The command to do so is:</p>
<pre><code class="lang-plaintext">$ npx create-next-app@latest
</code></pre>
<p>The result of the above command will ask you a series of questions to setup your app:</p>
<pre><code class="lang-plaintext">What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`?  No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
</code></pre>
<p>But for this tutorial, we aren’t interested in a full stack app – just an API. So let’s re-create the app using the <code>— - api</code> flag.</p>
<pre><code class="lang-plaintext">$ npx create-next-app@latest --api
</code></pre>
<p>It will still ask you a few questions. Use the default settings and finish creating the app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753245601007/61b34b8f-0426-4bf1-80ed-315f97e18d8a.png" alt="Next.js API setup" class="image--center mx-auto" width="1682" height="1208" loading="lazy"></p>
<p>Once the setup is done, you can see the folder with your app name. Let’s go into the folder and run the app.</p>
<pre><code class="lang-plaintext">$ npm run dev
</code></pre>
<p>Your API template should be running at port 3000. Go to <a target="_blank" href="http://localhost:3000/">http://localhost:3000</a> and you should see the following message:</p>
<pre><code class="lang-plaintext">{
"message": "Hello world!"
}
</code></pre>
<h2 id="heading-how-to-build-a-rest-api">How to Build a REST API</h2>
<p>Now that we’ve set up our API template, let's write a basic REST API. A basic REST API is simply four endpoints: Create, Read, Update, Delete (also called as CRUD).</p>
<p>Usually, we’d use a database, but for simplicity’s sake, we’ll use a JSON file in our API. Our goal is to build a REST API that can read and write to this JSON file.</p>
<p>The API code will reside under /app within the project directory. Next.js uses file-based routing for building URL paths.</p>
<p>For example, if you want a URL path /users, you should have a directory called “users” with a route.ts file to handle all the CRUD operations for /users. For /users/:id, you should have a directory called [id] under “users” directory with a route.ts file. The square brackets are to tell Next.js that you expect dynamic values for the /users/:id route.</p>
<p>You should also have the users.json inside the /app/users directory for your routes to read and write data.</p>
<p>Here is a screenshot of the setup. Delete the [slug] directory that comes with the project since it won’t be relevant for us:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753419862976/1ac6e871-6837-44e9-a02e-4e8d37ac4c76.png" alt="Route Structure" class="image--center mx-auto" width="360" height="384" loading="lazy"></p>
<ul>
<li><p>The route.ts file at the bottom handles CRUD operations for /</p>
</li>
<li><p>The route.ts file under/users handles CRUD operations for /users</p>
</li>
<li><p>The route.ts file under /users/[id]/ handles CRUD operations under /users/:id where the ‘id’ will be a dynamic value.</p>
</li>
<li><p>The users.json under /users will be our data store.</p>
</li>
</ul>
<p>While this setup can seem complicated for a simple project, it provides a clear structure for large-scale web applications. If you want to go deeper into building complex APIs with Next.js, <a target="_blank" href="https://nextjs.org/blog/building-apis-with-nextjs">here is a tutorial</a> you can follow.</p>
<p>The code under /app/route.ts is the default file for our API. You can see it serving the GET request and responding with “Hello World!”:</p>
<pre><code class="lang-plaintext">import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({ message: "Hello world!" });
}
</code></pre>
<p>Now we need five routes:</p>
<ul>
<li><p>GET /users → List all users</p>
</li>
<li><p>GET /users/:id → List a single user</p>
</li>
<li><p>POST /users → Create a new user</p>
</li>
<li><p>PUT /users/:id → Update an existing user</p>
</li>
<li><p>DELETE /users/:id → Delete an existing user</p>
</li>
</ul>
<p>Here is the code for the route.ts file under /app/users:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { promises <span class="hljs-keyword">as</span> fs } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>; <span class="hljs-comment">// Importing promise-based filesystem methods</span>
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>; <span class="hljs-comment">// For handling file paths</span>

<span class="hljs-comment">// Define the structure of a User object</span>
<span class="hljs-keyword">interface</span> User {
  id: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  age: <span class="hljs-built_in">number</span>;
}

<span class="hljs-comment">// Define the path to the users.json file</span>
<span class="hljs-keyword">const</span> usersFile = path.join(process.cwd(), <span class="hljs-string">"app/users/users.json"</span>);

<span class="hljs-comment">// Read users from the JSON file and return them as an array</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readUsers</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">User</span>[]&gt; </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fs.readFile(usersFile, <span class="hljs-string">"utf-8"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data) <span class="hljs-keyword">as</span> User[];
  } <span class="hljs-keyword">catch</span> {
    <span class="hljs-comment">// If file doesn't exist or fails to read, return empty array</span>
    <span class="hljs-keyword">return</span> [];
  }
}

<span class="hljs-comment">// Write updated users array to the JSON file</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">writeUsers</span>(<span class="hljs-params">users: User[]</span>) </span>{
  <span class="hljs-keyword">await</span> fs.writeFile(usersFile, <span class="hljs-built_in">JSON</span>.stringify(users, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>), <span class="hljs-string">"utf-8"</span>);
}

<span class="hljs-comment">// Handle GET request: return list of users</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();
  <span class="hljs-keyword">return</span> NextResponse.json(users);
}

<span class="hljs-comment">// Handle POST request: add a new user</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

  <span class="hljs-comment">// Destructure and validate input fields</span>
  <span class="hljs-keyword">const</span> { name, email, age } = body <span class="hljs-keyword">as</span> {
    name?: <span class="hljs-built_in">string</span>;
    email?: <span class="hljs-built_in">string</span>;
    age?: <span class="hljs-built_in">number</span>;
  };

  <span class="hljs-comment">// Return 400 if any required field is missing</span>
  <span class="hljs-keyword">if</span> (!name || !email || age === <span class="hljs-literal">undefined</span>) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">"Missing name, email, or age"</span> },
      { status: <span class="hljs-number">400</span> }
    );
  }

  <span class="hljs-comment">// Read existing users</span>
  <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();

  <span class="hljs-comment">// Create new user object with unique ID based on timestamp</span>
  <span class="hljs-keyword">const</span> newUser: User = {
    id: <span class="hljs-built_in">Date</span>.now().toString(),
    name,
    email,
    age,
  };

  <span class="hljs-comment">// Add new user to the list and save to file</span>
  users.push(newUser);
  <span class="hljs-keyword">await</span> writeUsers(users);

  <span class="hljs-comment">// Return the newly created user with 201 Created status</span>
  <span class="hljs-keyword">return</span> NextResponse.json(newUser, { status: <span class="hljs-number">201</span> });
}
</code></pre>
<p>Now the code for the /app/users/[id]/route.ts file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { promises <span class="hljs-keyword">as</span> fs } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;

<span class="hljs-comment">// Define the User interface</span>
<span class="hljs-keyword">interface</span> User {
  id: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  age: <span class="hljs-built_in">number</span>;
}

<span class="hljs-comment">// Path to the users.json file</span>
<span class="hljs-keyword">const</span> usersFile = path.join(process.cwd(), <span class="hljs-string">"app/users/users.json"</span>);

<span class="hljs-comment">// Function to read users from the JSON file</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readUsers</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">User</span>[]&gt; </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fs.readFile(usersFile, <span class="hljs-string">"utf-8"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data) <span class="hljs-keyword">as</span> User[];
  } <span class="hljs-keyword">catch</span> {
    <span class="hljs-comment">// If file doesn't exist or is unreadable, return an empty array</span>
    <span class="hljs-keyword">return</span> [];
  }
}

<span class="hljs-comment">// Function to write updated users to the JSON file</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">writeUsers</span>(<span class="hljs-params">users: User[]</span>) </span>{
  <span class="hljs-keyword">await</span> fs.writeFile(usersFile, <span class="hljs-built_in">JSON</span>.stringify(users, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>), <span class="hljs-string">"utf-8"</span>);
}

<span class="hljs-comment">// GET /users/:id - Fetch a user by ID</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: <span class="hljs-built_in">Promise</span>&lt;{ id: <span class="hljs-built_in">string</span> }&gt; },
</span>) </span>{
  <span class="hljs-keyword">const</span> id = (<span class="hljs-keyword">await</span> params).id;
  <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();

  <span class="hljs-comment">// Find the user by ID</span>
  <span class="hljs-keyword">const</span> user = users.find(<span class="hljs-function">(<span class="hljs-params">u</span>) =&gt;</span> u.id === id);

  <span class="hljs-comment">// Return 404 if user is not found</span>
  <span class="hljs-keyword">if</span> (!user) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"User not found"</span> }, { status: <span class="hljs-number">404</span> });
  }

  <span class="hljs-comment">// Return the found user</span>
  <span class="hljs-keyword">return</span> NextResponse.json(user);
}

<span class="hljs-comment">// PUT /users/:id - Update a user by ID</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PUT</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: <span class="hljs-built_in">Promise</span>&lt;{ id: <span class="hljs-built_in">string</span> }&gt; },
</span>) </span>{
  <span class="hljs-keyword">const</span> id = (<span class="hljs-keyword">await</span> params).id;
  <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

  <span class="hljs-comment">// Extract optional fields from request body</span>
  <span class="hljs-keyword">const</span> { name, email, age } = body <span class="hljs-keyword">as</span> {
    name?: <span class="hljs-built_in">string</span>;
    email?: <span class="hljs-built_in">string</span>;
    age?: <span class="hljs-built_in">number</span>;
  };

  <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();

  <span class="hljs-comment">// Find the index of the user to update</span>
  <span class="hljs-keyword">const</span> index = users.findIndex(<span class="hljs-function">(<span class="hljs-params">u</span>) =&gt;</span> u.id === id);

  <span class="hljs-comment">// Return 404 if user not found</span>
  <span class="hljs-keyword">if</span> (index === <span class="hljs-number">-1</span>) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"User not found"</span> }, { status: <span class="hljs-number">404</span> });
  }

  <span class="hljs-comment">// Update the user only with provided fields</span>
  users[index] = {
    ...users[index],
    ...(name !== <span class="hljs-literal">undefined</span> ? { name } : {}),
    ...(email !== <span class="hljs-literal">undefined</span> ? { email } : {}),
    ...(age !== <span class="hljs-literal">undefined</span> ? { age } : {}),
  };

  <span class="hljs-keyword">await</span> writeUsers(users);

  <span class="hljs-comment">// Return the updated user</span>
  <span class="hljs-keyword">return</span> NextResponse.json(users[index]);
}

<span class="hljs-comment">// DELETE /users/:id - Delete a user by ID</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DELETE</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: <span class="hljs-built_in">Promise</span>&lt;{ id: <span class="hljs-built_in">string</span> }&gt; },
</span>) </span>{
  <span class="hljs-keyword">const</span> id = (<span class="hljs-keyword">await</span> params).id;
  <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();

  <span class="hljs-comment">// Find the index of the user to delete</span>
  <span class="hljs-keyword">const</span> index = users.findIndex(<span class="hljs-function">(<span class="hljs-params">u</span>) =&gt;</span> u.id === id);

  <span class="hljs-comment">// Return 404 if user not found</span>
  <span class="hljs-keyword">if</span> (index === <span class="hljs-number">-1</span>) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"User not found"</span> }, { status: <span class="hljs-number">404</span> });
  }

  <span class="hljs-comment">// Remove user from the array and save updated list</span>
  <span class="hljs-keyword">const</span> [deleted] = users.splice(index, <span class="hljs-number">1</span>);
  <span class="hljs-keyword">await</span> writeUsers(users);

  <span class="hljs-comment">// Return the deleted user</span>
  <span class="hljs-keyword">return</span> NextResponse.json(deleted);
}
</code></pre>
<p>We will have an empty array inside the /app/users.json. You can find all the code here <a target="_blank" href="https://github.com/manishmshiva/nextjs-api">in this repository</a>.</p>
<h2 id="heading-how-to-test-the-api">How to Test the API</h2>
<p>Now let’s test the API endpoints.</p>
<p>First, lets run the API:</p>
<pre><code class="lang-typescript">$ npm run dev
</code></pre>
<p>You can go to <a target="_blank" href="http://localhost:3000/users">http://localhost:3000/users</a> and can see an empty array since we have not pushed any user information.</p>
<p>From the code, we can see that a user object needs name, email, and age since the id is automatically generated in the POST endpoint.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753336220369/11fceed9-6b2e-45f7-a5f9-2159d85e3cfb.png" alt="New user creation" class="image--center mx-auto" width="1426" height="420" loading="lazy"></p>
<p>We will use <a target="_blank" href="https://www.postman.com/downloads/">Postman</a> to simulate requests to the API and ensure that the API behaves as expected.</p>
<ol>
<li>GET /users: it will be empty on our first try since we haven’t pushed any data yet.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753340954122/794e41d4-2660-49de-8bff-dbd979979049.png" alt="GET /users" class="image--center mx-auto" width="1708" height="1270" loading="lazy"></p>
<ol start="2">
<li>POST /users: create a new user. Under “body”, choose “raw” and select “JSON”. This is the data we will be sending the api. The JSON body would be</li>
</ol>
<pre><code class="lang-typescript">{<span class="hljs-string">"name"</span>:<span class="hljs-string">"Manish"</span>,<span class="hljs-string">"age"</span>:<span class="hljs-number">30</span>, <span class="hljs-string">"email"</span>:<span class="hljs-string">"manish@example.com"</span>}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753341103739/96f42da7-127c-4e99-9cd2-054d8bb74633.png" alt="POST /users" class="image--center mx-auto" width="1736" height="1280" loading="lazy"></p>
<p>I’ll create one more record named “Larry”. Here is the JSON:</p>
<pre><code class="lang-typescript">{<span class="hljs-string">"name"</span>:<span class="hljs-string">"Larry"</span>,<span class="hljs-string">"age"</span>:<span class="hljs-number">25</span>, <span class="hljs-string">"email"</span>:<span class="hljs-string">"larrry@example.com"</span>}
</code></pre>
<p>Now let’s look at the users. You should see two entries for our GET request to /users:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753341200059/e4237349-9fc6-4424-965a-f4dc2d2cda3f.png" alt="GET /users" class="image--center mx-auto" width="1710" height="1270" loading="lazy"></p>
<p>Now let’s look at a single user using /users/:id.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753341556154/8dda05a1-7a83-40a2-b212-cd5ccb54f0a3.png" alt="GET /users/:id" class="image--center mx-auto" width="1690" height="1240" loading="lazy"></p>
<p>Now let’s update Larry’s age to 35. We’ll pass just the age in request body using the PUT request to /users/:id.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753341614641/a5a7ffbd-a371-42f9-9a32-285aeec39066.png" alt="PUT /users/:id" class="image--center mx-auto" width="1722" height="1224" loading="lazy"></p>
<p>Now let’s delete Larry’s record.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753341668605/9456cf8d-e694-4d1d-968e-3ab9a9c6c30e.png" alt="DELETE /users/:id" class="image--center mx-auto" width="1702" height="744" loading="lazy"></p>
<p>If you check /users, you should see only one record:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753341735968/d0b6ecf9-34e4-49a2-b6e1-da0ab859a8b3.png" alt="GET /users" class="image--center mx-auto" width="1736" height="1258" loading="lazy"></p>
<p>So we have built and tested our api. Now let’s get this live.</p>
<h2 id="heading-how-to-deploy-to-sevalla">How to Deploy to Sevalla</h2>
<p><a target="_blank" href="https://sevalla.com/">Sevalla</a> is a modern, usage-based Platform-as-a-service provider and an alternative to sites like Heroku or to your self-managed setup on AWS. It combines powerful features with a smooth developer experience.</p>
<p>Sevalls offers application hosting, database, object storage, and static site hosting for your projects. It comes with a generous free tier, so let’s see how to deploy our API to the cloud using Sevalla.</p>
<p>Make sure you have the code committed to GitHub or <a target="_blank" href="https://github.com/manishmshiva/nextjs-api">fork my repository</a> for this project. If you are new to Sevalla, you can sign up using your GitHub account to enable direct deploys from your GitHub account. Every time you push code to your project, Sevalla will auto-pull and deploy your app to the cloud.</p>
<p>Once you <a target="_blank" href="https://app.sevalla.com/login">login</a> to Sevalla, click on “Applications”. Now let’s create an app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753420176622/a96c398e-18bc-42c2-917a-ba6a4f9ed585.png" alt="Sevalla App interface" class="image--center mx-auto" width="2942" height="1324" loading="lazy"></p>
<p>If you have authenticated with GitHub, the application creation interface will display a list of repositories. Choose the one you pushed your code into or the nextjs-api project if you forked it from my repository.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753420351315/cc8b072b-fda8-43d5-a56b-9e3dd623523d.png" alt="Connect Repository" class="image--center mx-auto" width="1498" height="388" loading="lazy"></p>
<p>Check the box “auto deploy on commit”. This will ensure your latest code is auto-deployed to Sevalla. Now, let’s choose the instance to which we can deploy the application. Each one comes with its own pricing, based on the server's capacity.</p>
<p>Let’s choose the hobby server that costs $5/mo. Sevalla gives us a $50 free tier, so we don’t have to pay for anything unless we exceed this usage tier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753420434828/71cb0e8e-832a-4077-b493-6928c874eb4b.png" alt="Sevalla usage tier" class="image--center mx-auto" width="1320" height="290" loading="lazy"></p>
<p>Now, click “Create and Deploy”. This should pull our code from our repository, run the build process, setup a Docker container and then deploy the app. Usually the work of a sysadmin, fully automated by Sevalla.</p>
<p>Wait for a few minutes for all the above to happen. You can watch the logs in the “Deployments” interface.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753424002440/8dc98179-15f2-4178-b285-23f0f475bc66.png" alt="Application deployment" class="image--center mx-auto" width="2426" height="1678" loading="lazy"></p>
<p>Now, click on “Visit App” and you will get the live URL (ending with sevalla.app) of your API. You can replace “http://localhost:3000” with the new URL and run the same tests using Postman.</p>
<p>Congratulations – your app is now live. You can do more with your app using the admin interface, like:</p>
<ul>
<li><p>Monitor the performance of your app</p>
</li>
<li><p>Watch real-time logs</p>
</li>
<li><p>Add custom domains</p>
</li>
<li><p>Update network settings (open/close ports for security, and so on)</p>
</li>
<li><p>Add more storage</p>
</li>
</ul>
<p>Sevalla also provides resources like Object storage, database, cache, and so on, which are out of scope for this tutorial. But it lets you monitor, manage, and scale your application without the need for a system administrator. That’s the beauty of PaaS systems. Here is a detailed comparison of <a target="_blank" href="https://www.freecodecamp.org/news/vps-vs-paas-how-to-choose-a-hosting-solution/">VPS vs PaaS systems</a> for application hosting.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this article, we went beyond the typical frontend use case of Next.js and explored its capabilities as a full-stack framework. We built a complete REST API using the App Router and file-based routing, with data stored in a JSON file. Then, we took it a step further and deployed the API to production using Sevalla, a modern PaaS that automates deployment, scaling, and monitoring.</p>
<p>This setup demonstrates how developers can build and ship full-stack applications like frontend, backend, and deployment, all within a single Next.js project. Whether you're prototyping or building for scale, this workflow sets you up with everything you need to get your apps into users’ hands quickly and efficiently.</p>
<p>Hope you enjoyed this article. I ll see you soon with another one. <a target="_blank" href="https://manishshivanandhan.com/">Connect with me on LinkedIn</a> or <a target="_blank" href="https://linkedin.com/in/manishmshiva">visit my website</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How To Deploy a Next.js App To Vercel With GitHub Actions ]]>
                </title>
                <description>
                    <![CDATA[ Vercel is a cloud platform or Platform-as-a-Service (PaaS) designed to help frontend developers create, preview, and deploy web applications swiftly and efficiently. In this tutorial, we’ll focus on deploying a Next.js application to Vercel using Git... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/deploy-to-vercel-with-github-actions/</link>
                <guid isPermaLink="false">684871b16bc1898625c952fd</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub Actions ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chidiadi Anyanwu ]]>
                </dc:creator>
                <pubDate>Tue, 10 Jun 2025 17:56:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749577622920/8e35a6c1-3f4f-49a3-a4fe-dba80e24eec3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Vercel is a cloud platform or Platform-as-a-Service (PaaS) designed to help frontend developers create, preview, and deploy web applications swiftly and efficiently. In this tutorial, we’ll focus on deploying a Next.js application to Vercel using GitHub Actions.</p>
<p>In a <a target="_blank" href="https://www.freecodecamp.org/news/how-to-build-a-simple-portfolio-blog-with-nextjs/">previous article</a>, we built a Next.js portfolio blog. Here, you’ll learn how to deploy it on Vercel with <a target="_blank" href="https://www.freecodecamp.org/news/automate-cicd-with-github-actions-streamline-workflow/">GitHub Actions</a>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To be able to deploy your project, you should have a GitHub repository of the project (you can still follow along if you already have a Next.js project), and a Vercel account. <a target="_blank" href="https://github.com/chidiadi01/simple-writer-portfolio">Here is the GitHub repository that we’ll be working with</a>. You can clone it to follow along.</p>
<h2 id="heading-how-to-deploy-your-next-app">How to Deploy Your Next App</h2>
<h3 id="heading-create-vercel-token-and-add-it-to-your-secrets-in-github">Create Vercel Token and Add it to Your Secrets in GitHub</h3>
<p>In your Vercel account, go to Settings, then go to Tokens.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749036906930/1c483351-0e78-4392-a948-53921ba2916c.png" alt="Vercel account settings tokens." class="image--center mx-auto" width="1354" height="602" loading="lazy"></p>
<p>In the Create Token section, enter a name for your token, select an expiration date and click “create”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749037009419/2a48b48d-31c4-4b72-a281-dd8eb689770d.png" alt="Creating a vercel token" class="image--center mx-auto" width="1347" height="593" loading="lazy"></p>
<p>You should see a success message with your token. Next, go to your GitHub repository, and click on the “Settings“ tab.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749037335441/1a46fb8b-8f3c-4d44-8ad5-c7de3340ef2b.png" alt="Vercel, token created success message." class="image--center mx-auto" width="1339" height="599" loading="lazy"></p>
<p>In the Settings tab, go to Secrets and Variables on the sidebar, then click on Actions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749037726010/5ca33111-0dbe-4e3e-bde5-91a8e518f05b.png" alt="Actions secrets in GitHub repository settings." class="image--center mx-auto" width="1100" height="601" loading="lazy"></p>
<p>You’ll see a section for adding secrets. Add a secret named <code>VERCEL_TOKEN</code>, and paste the token there.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749037787198/53b42a96-ad79-4895-aba0-abe6d79eceb5.png" alt="vercel token, project id, org id." class="image--center mx-auto" width="880" height="334" loading="lazy"></p>
<p>The Vercel token is a token used to authenticate the GitHub runner. The Vercel CLI installed on the GitHub runner is going to execute the commands with your account. So, instead of it having to login, it uses the access token to verify that it was actually authorized by you to take the actions.</p>
<p>The Organization ID is used to tell Vercel which organization or team account the project should be created under.</p>
<p>The Project ID then tells Vercel the specific project you want to deploy. Just like the Organization ID, it is a unique identifier.</p>
<h3 id="heading-install-the-vercel-cli-and-login">Install the Vercel CLI and Login</h3>
<p>Use the command below to install vercel CLI globally on your computer:</p>
<pre><code class="lang-bash">npm install -g vercel
</code></pre>
<p>Then <a target="_blank" href="https://vercel.com/docs/cli/login">log into the CLI</a> with the following command:</p>
<pre><code class="lang-bash">vercel login
</code></pre>
<p>Use one of the options to login.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749312895981/93c13b75-83da-4da7-b5c5-17572d126ce4.png" alt="login methods" class="image--center mx-auto" width="762" height="159" loading="lazy"></p>
<p>I used GitHub. Select one with your arrow keys, and click enter.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749312974078/2f470d5a-9e73-44be-b520-5179da61f86b.png" alt="login success" class="image--center mx-auto" width="1335" height="571" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749038078744/bbb5a43d-66a4-4531-a27c-12442c977568.png" alt="vercel login" class="image--center mx-auto" width="921" height="140" loading="lazy"></p>
<h3 id="heading-create-a-vercel-project-from-your-local-directory">Create a Vercel Project from Your Local Directory</h3>
<p>Navigate to your project directory if you’re not already in it. If you have already created a project on Vercel through the web interfce, use the <a target="_blank" href="https://vercel.com/docs/cli/link">vercel link</a> command to link your current directory to the Vercel project. If you don’t already have a Vercel project, just type <code>vercel</code> in the CLI and follow the prompts to setup the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749314606959/f6d7a71c-edc5-48f2-81ca-4fd3a92c44da.png" alt="Create new Vercel project" class="image--center mx-auto" width="604" height="52" loading="lazy"></p>
<p>With that, Vercel will create a <code>.vercel</code> folder in the project. Open it, and go to the <code>project.json</code> file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749038798352/23d8b1b9-5bbf-43df-9444-7adf1f7a9c2f.png" alt="Project.json" class="image--center mx-auto" width="232" height="105" loading="lazy"></p>
<p>In the file, you should see your project ID and organization ID. Copy them and create secrets in your GitHub repository for each one.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749037787198/53b42a96-ad79-4895-aba0-abe6d79eceb5.png" alt="vercel token, org ID, project ID." class="image--center mx-auto" width="880" height="334" loading="lazy"></p>
<h3 id="heading-create-your-github-workflow-file">Create your GitHub Workflow File</h3>
<p>At the root of your project folder, create the <code>.github/workflow</code> folder. Then create a workflow file called <code>vercel_deploy.yml</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749039652451/f3c3a4ca-5d19-4866-a69d-9f4d3a760d8f.png" alt="f3c3a4ca-5d19-4866-a69d-9f4d3a760d8f" class="image--center mx-auto" width="233" height="45" loading="lazy"></p>
<p>In the file, write this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Vercel</span> <span class="hljs-string">Production</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">env:</span>
  <span class="hljs-attr">VERCEL_ORG_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.VERCEL_ORG_ID</span> <span class="hljs-string">}}</span>
  <span class="hljs-attr">VERCEL_PROJECT_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.VERCEL_PROJECT_ID</span> <span class="hljs-string">}}</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'01-simple-blog/**'</span>  

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">Deploy-Production:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">defaults:</span>
      <span class="hljs-attr">run:</span>
        <span class="hljs-attr">working-directory:</span> <span class="hljs-number">01</span><span class="hljs-string">-simple-blog</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Vercel</span> <span class="hljs-string">CLI</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span> <span class="hljs-string">--global</span> <span class="hljs-string">vercel@latest</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Pull</span> <span class="hljs-string">Vercel</span> <span class="hljs-string">Environment</span> <span class="hljs-string">Information</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">vercel</span> <span class="hljs-string">pull</span> <span class="hljs-string">--yes</span> <span class="hljs-string">--environment=production</span> <span class="hljs-string">--token=${{</span> <span class="hljs-string">secrets.VERCEL_TOKEN</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">Project</span> <span class="hljs-string">Artifacts</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">vercel</span> <span class="hljs-string">build</span> <span class="hljs-string">--prod</span> <span class="hljs-string">--token=${{</span> <span class="hljs-string">secrets.VERCEL_TOKEN</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">continue-on-error:</span> <span class="hljs-literal">true</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">Project</span> <span class="hljs-string">Artifacts</span> <span class="hljs-string">to</span> <span class="hljs-string">Vercel</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">vercel</span> <span class="hljs-string">deploy</span> <span class="hljs-string">--prebuilt</span> <span class="hljs-string">--prod</span> <span class="hljs-string">--token=${{</span> <span class="hljs-string">secrets.VERCEL_TOKEN</span> <span class="hljs-string">}}</span>
</code></pre>
<p>This is the workflow file for my <a target="_blank" href="https://github.com/chidiadi01/simple-writer-portfolio/blob/main/.github/workflows/vercel_deploy.yml">simple-writer-portfolio</a> project.</p>
<p>First, we have the environment variables:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">env:</span>
  <span class="hljs-attr">VERCEL_ORG_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.VERCEL_ORG_ID</span> <span class="hljs-string">}}</span>
  <span class="hljs-attr">VERCEL_PROJECT_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.VERCEL_PROJECT_ID</span> <span class="hljs-string">}}</span>
<span class="hljs-comment"># Other code</span>
</code></pre>
<p>Then we have the trigger. This triggers when I push to the main branch, affecting files in the <code>01-simple-blog</code> subdirectory.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Previous code</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'01-simple-blog/**'</span>  
<span class="hljs-comment"># Other code</span>
</code></pre>
<p>Then we have the job definition. Here, I defined a job “Deploy-Production” that runs on Ubuntu. By default, all commands there will run in the <code>01-simple-blog</code> directory, which is equivalent to running <code>cd 01-simple-blog</code> from the root before running commands on the shell. I did this because the Next.js project is in that directory, where the <code>package.json</code> is located.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Previous code</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">Deploy-Production:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">defaults:</span>
      <span class="hljs-attr">run:</span>
        <span class="hljs-attr">working-directory:</span> <span class="hljs-number">01</span><span class="hljs-string">-simple-blog</span>
<span class="hljs-comment"># Other code</span>
</code></pre>
<p>Then the steps involved:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Previous code</span>
 <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Vercel</span> <span class="hljs-string">CLI</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span> <span class="hljs-string">--global</span> <span class="hljs-string">vercel@latest</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Pull</span> <span class="hljs-string">Vercel</span> <span class="hljs-string">Environment</span> <span class="hljs-string">Information</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">vercel</span> <span class="hljs-string">pull</span> <span class="hljs-string">--yes</span> <span class="hljs-string">--environment=production</span> <span class="hljs-string">--token=${{</span> <span class="hljs-string">secrets.VERCEL_TOKEN</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">Project</span> <span class="hljs-string">Artifacts</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">vercel</span> <span class="hljs-string">build</span> <span class="hljs-string">--prod</span> <span class="hljs-string">--token=${{</span> <span class="hljs-string">secrets.VERCEL_TOKEN</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">continue-on-error:</span> <span class="hljs-literal">true</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">Project</span> <span class="hljs-string">Artifacts</span> <span class="hljs-string">to</span> <span class="hljs-string">Vercel</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">vercel</span> <span class="hljs-string">deploy</span> <span class="hljs-string">--prebuilt</span> <span class="hljs-string">--prod</span> <span class="hljs-string">--token=${{</span> <span class="hljs-string">secrets.VERCEL_TOKEN</span> <span class="hljs-string">}}</span>
</code></pre>
<p>With these steps, Vercel is first installed on the GitHub runner. Then the vercel environment information is pulled. The project is built with <code>vercel build</code>, and the pre-built artifacts are then pushed to Vercel.</p>
<h3 id="heading-push-to-github-and-watch-your-code-deploy">Push to GitHub and watch your code deploy</h3>
<p>Stage your changes, if any:</p>
<pre><code class="lang-bash">git add .
</code></pre>
<p>Commit the changes:</p>
<pre><code class="lang-bash">git commit -m <span class="hljs-string">"Added GitHub Actions workflow"</span>
</code></pre>
<p>And push:</p>
<pre><code class="lang-bash">git push origin
</code></pre>
<p>Now, go to your repository online, and check the deployment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749042451313/3fa5a9a8-c14b-4f55-9e04-c51310500c3f.png" alt="3fa5a9a8-c14b-4f55-9e04-c51310500c3f" class="image--center mx-auto" width="899" height="46" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749042384631/d941710c-697d-46b8-a106-30f6ac3cedc3.png" alt="Workflow run logs" class="image--center mx-auto" width="981" height="492" loading="lazy"></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>With your basic GitHub workflow in place, you can now make changes to your code, push to GitHub, and have it deploy automatically. Though Vercel allows you to connect your repository directly, this method provides you with more flexibility and customizability. If you enjoyed this article, share it with others. You can also reach me on <a target="_blank" href="https://linkedin.com/in/chidiadi-anyanwu">LinkedIn</a> or <a target="_blank" href="https://x.com/chidiadi01">X</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Best AWS Services to Deploy Front-End Applications in 2025 ]]>
                </title>
                <description>
                    <![CDATA[ As front-end development evolves, finding the right deployment service is more important than ever. Amazon Web Services (AWS), a cloud-based service, offers a number of helpful tools and platforms for hosting modern front-end applications. Although i... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/best-aws-services-for-frontend-deployment/</link>
                <guid isPermaLink="false">68363d68d27e47d637cc580b</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Developer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ijeoma Igboagu ]]>
                </dc:creator>
                <pubDate>Tue, 27 May 2025 22:32:08 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748382386996/10b7c32c-f456-4717-b37c-b1668565bede.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As front-end development evolves, finding the right deployment service is more important than ever. Amazon Web Services (AWS), a cloud-based service, offers a number of helpful tools and platforms for hosting modern front-end applications. Although it may present challenges for beginners, AWS can help give companies an edge with its global reach.</p>
<p>In this article, I’ll break down the best AWS services for frontend deployment in 2025, covering their use cases and their pros and cons. Whether you’re launching a static website, a React/Vue application, or a complex web application, this article will help you find the most effective AWS solution for your needs.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-choose-aws-for-frontend-hosting">Why Choose AWS for Frontend Hosting?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-aws-services-for-frontend-hosting">AWS Services for Frontend Hosting</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-amazon-s3-simple-storage-service">Amazon S3 (Simple Storage Service)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-aws-elastic-beanstalk">AWS Elastic Beanstalk</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-amazon-ec2-elastic-compute-cloud">Amazon EC2 (Elastic Compute Cloud)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-aws-amplify">AWS Amplify</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-aws-lightsail">AWS LightSail</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-aws-app-runner">AWS App Runner</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-related-articles">Related Articles</a></p>
</li>
</ul>
<h2 id="heading-why-choose-aws-for-frontend-hosting">Why Choose AWS for Frontend Hosting?</h2>
<p>Before we explore the specific AWS services for frontend hosting, let’s first look at why developers and companies often choose AWS over more familiar platforms like <a target="_blank" href="https://www.netlify.com/">Netlify</a> or <a target="_blank" href="https://vercel.com/">Vercel</a>.</p>
<ul>
<li><p>AWS has data centers worldwide, reducing latency and ensuring high availability. This means that apps deployed using their services can be easily and quickly accessed anywhere in the world (as long as that region has a nearby data center).</p>
</li>
<li><p>Any provisioned AWS service has security features like encryption, IAM, and DDoS protection.</p>
</li>
<li><p>AWS automatically scales to handle high traffic spikes without downtime.</p>
</li>
<li><p>AWS is flexible – it supports frameworks like React, Vue, Angular, and Next.js.</p>
</li>
<li><p>AWS easily integrates with other AWS services like databases (DynamoDB, RDS), APIs (API Gateway), and authentication (Cognito).</p>
</li>
</ul>
<h2 id="heading-aws-services-for-frontend-hosting">AWS Services for Frontend Hosting</h2>
<h3 id="heading-amazon-s3-simple-storage-service"><strong>Amazon S3 (Simple Storage Service)</strong></h3>
<p>Amazon S3 is a storage service from AWS mainly used to store files like HTML, CSS, JavaScript, Images, and Videos. These files make up static websites – that is, websites that don’t change based on user actions.</p>
<p>Many developers use S3 to host their static websites because it’s reliable, it works well, and it doesn’t cost much. You just upload your files to an S3 bucket, make them public, and your website is live. You can also connect a custom domain and add extra features like faster loading through a CDN (like CloudFront).</p>
<p><strong>Use Case:</strong><br>AWS S3 is perfect for hosting static websites and storing media files, such as portfolio sites, blogs, documentation pages, or any site that doesn't require a server to run backend code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746277600652/c2a29581-edd7-478e-8993-9bf1d951aab8.png" alt="c2a29581-edd7-478e-8993-9bf1d951aab8" class="image--center mx-auto" width="1767" height="343" loading="lazy"></p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Easy to use and affordable for most projects.</p>
</li>
<li><p>Keeps your files available almost all the time.</p>
</li>
<li><p>Your data is stored safely and can be backed up automatically.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>It doesn’t support backend features like running code, handling forms, or connecting to databases.</li>
</ul>
<p>To help you get started using S3 for hosting, here’s an article that explains <a target="_blank" href="https://www.freecodecamp.org/news/host-a-static-website-on-aws-s3-and-cloudfront/">how to host a static website using AWS S3 and Cloudfront</a>.</p>
<h3 id="heading-aws-elastic-beanstalk"><strong>AWS Elastic Beanstalk</strong></h3>
<p>AWS Elastic Beanstalk is a service that helps you quickly deploy and manage web applications without needing to handle the underlying infrastructure. While it’s often used for backend services, it also works well for frontend apps that need server-side features.</p>
<p><strong>Use Case:</strong><br>AWS Elastic Beanstalk is ideal for hosting full-stack applications, especially those built with server-side frameworks like Next.js or Nuxt.js. It handles both the frontend and backend in one environment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746279088455/a417bf0a-8cb6-4d1e-bddb-33a470e082f9.png" alt="Using AWS Elastic BeanStalk to create a full-stack application" class="image--center mx-auto" width="1891" height="413" loading="lazy"></p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Automatically scales your app based on traffic</p>
</li>
<li><p>Includes load balancing to manage high traffic smoothly</p>
</li>
<li><p>Works well with other AWS tools like RDS for databases and CloudWatch for monitoring</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>More complex to set up compared to simpler services like Amplify or S3</p>
</li>
<li><p>Not the best choice for static websites that don’t require server-side logic</p>
</li>
</ul>
<p>To better understand how it works, I used AWS Elastic Beanstalk to set up a CI/CD pipeline. You can read this article: <a target="_blank" href="https://dev.to/ijay/how-to-create-a-cicd-using-aws-elastic-beanstalk-15nh">How to Create a CI/CD Using AWS Elastic BeanStalk</a>.</p>
<h3 id="heading-amazon-ec2-elastic-compute-cloud"><strong>Amazon EC2 (Elastic Compute Cloud)</strong></h3>
<p>Amazon EC2 lets you run your virtual server in the cloud. You can install any software, upload a website, open or close ports, and have full control over how your application runs. It's similar to having your physical computer, but it's hosted online and it’s more flexible.</p>
<p><strong>Use Case:</strong><br>AWS EC2 is great for developers or teams who need full control over their hosting setup. It's useful for projects where the frontend and backend are closely connected, or where you need to run custom services, tools, or configurations that simpler platforms can’t handle.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746280458269/6acafa6a-70f5-4a31-807c-de3cc3c09dfa.png" alt="using the EC2 for running your web application" class="image--center mx-auto" width="1542" height="394" loading="lazy"></p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Full control over the server environment.</p>
</li>
<li><p>Supports custom setups, tools, and applications.</p>
</li>
<li><p>It is a flexible way to run any application on it.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>Takes time to learn and manage.</p>
</li>
<li><p>You’re responsible for handling updates, scaling, and security.</p>
</li>
</ul>
<p>To help you understand how it works and how to connect it to your code editor (IDE), here's an article that walks you through the process: <a target="_blank" href="https://www.freecodecamp.org/news/how-to-connect-your-aws-ec2-instance-to-vs-code/">How to Connect Your AWS EC2 Instance to VS Code</a>.</p>
<h3 id="heading-aws-amplify"><strong>AWS Amplify</strong></h3>
<p>AWS Amplify is a platform that makes it easy to build and host frontend and mobile applications. It’s designed for developers working with frameworks like React, Vue, Angular, or Next.js.</p>
<p>Amplify handles things like hosting, authentication, APIs, and data storage. It supports Git-based CI/CD, which means your app can automatically update every time you push code. It comes with built-in support for popular tools like Cognito (for login systems), AppSync (for APIs), and DynamoDB (for databases). You can even create different environments based on your Git branches.</p>
<p><strong>Use Case:</strong><br>AWS Amplify is ideal for teams or solo developers building full-stack apps with modern frontend tools, especially when you want built-in features like user authentication, cloud APIs, and easy deployment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746282073570/9392f80c-aede-400a-a8f8-207167f44545.png" alt="using amplify for frontend or full-stack application" class="image--center mx-auto" width="1811" height="797" loading="lazy"></p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Simple full-stack hosting – frontend and backend in one place.</p>
</li>
<li><p>Fast setup with automatic scaling.</p>
</li>
<li><p>Comes with HTTPS, custom domain setup, and performance monitoring.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>More expensive than simpler solutions like S3 for basic static sites.</p>
</li>
<li><p>Less flexibility for developers who want full control over infrastructure.</p>
</li>
</ul>
<p>Here is a simple guide for building with AWS Amplify. I hope it helps you understand it better: <a target="_blank" href="https://dev.to/ijay/how-can-aws-amplify-improve-your-development-process-2gj5">How Can AWS Amplify Improve Your Development Process?</a></p>
<p>And here’s an in-depth guide that walks you through <a target="_blank" href="https://www.freecodecamp.org/news/ultimate-guide-to-aws-amplify-and-reacxt/">building a full-stack app with AWS Amplify and React</a>.</p>
<h3 id="heading-aws-lightsail"><strong>AWS LightSail</strong></h3>
<p>AWS LightSail is a beginner-friendly cloud hosting service that offers a quick and easy way to launch small applications. It works like a simpler version of EC2 and comes with pre-configured environments for Node.js, LAMP (Linux, Apache, Mysql, PHP), and WordPress. This means that you don’t have to spend time setting everything up from scratch.</p>
<p><strong>Use Case:</strong><br>It is perfect for freelancers, small businesses, or anyone who wants to host a simple website or app, such as a blog, a small web app, or an online portfolio.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746348715984/c013a2de-b004-4c44-a5c6-23ddb9226962.png" alt="using AWS light sail for your application" class="image--center mx-auto" width="290" height="512" loading="lazy"></p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>More affordable than EC2.</p>
</li>
<li><p>Easy to set up and manage.</p>
</li>
<li><p>Comes with ready-to-use application stacks.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>Not ideal for large or fast-growing projects.</p>
</li>
<li><p>Has fewer customisation and scaling options compared to EC2 or Amplify.</p>
</li>
</ul>
<p>For a fun, project-based tutorial, check out this guide that teaches you <a target="_blank" href="https://www.freecodecamp.org/news/how-do-deploy-docker-containers-to-the-cloud-with-aws-lightsail/">how to use AWS LightSail to deploy Docker containers to the cloud</a>.</p>
<h3 id="heading-aws-app-runner"><strong>AWS App Runner</strong></h3>
<p>AWS App Runner is a service that helps you run web applications without setting up or managing any servers. You just connect your source code or a container image, and App Runner handles everything. It's a quick way to get your frontend or backend app online, especially if your app needs server-side processing.</p>
<p><strong>Use Case:</strong><br>App Runner is a good choice for frontend applications built with server-side rendering (like Next.js), full-stack apps, or APIs. It’s also helpful if your app is containerised and you want it to scale automatically based on traffic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746351485218/748d06a5-b4dd-493d-b809-e72af84d0f5c.png" alt="Using AWS App Runner" class="image--center mx-auto" width="1628" height="360" loading="lazy"></p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>No server setup or management.</p>
</li>
<li><p>Automatically scales your app as needed.</p>
</li>
<li><p>Easy to connect with GitHub or Amazon ECR.</p>
</li>
<li><p>HTTPS and custom domain support included.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>It’s not the best choice for very simple websites that only show static content like HTML, CSS, and JavaScript files. Other services like S3 are easier and cheaper for that kind of site.</p>
</li>
<li><p>It doesn’t give you as much control as EC2 or ECS. For example, you can’t fully customise the server environment or how things run behind the scenes. So, it’s great for simple or standard setups, but not ideal if you need to fine-tune advanced settings.</p>
</li>
</ul>
<p>If you want to learn more about deploying apps with AppRunner, here’s a tutorial about <a target="_blank" href="https://www.freecodecamp.org/news/kotlin-aws-app-runner/">deploying a Kotlin microservice to AppRunner</a> that you can check out.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>AWS offers a variety of powerful tools for hosting frontend applications, from simple static site hosting on S3 to full-stack managed deployments with Amplify. Whether you’re a solo developer launching your portfolio or a team deploying a production web app, AWS has the flexibility and scalability to support your frontend needs.</p>
<p>By understanding each service’s purpose and use case, you can confidently pick the best fit for your project and scale as needed. Start small, experiment, and grow with AWS.</p>
<p>If you found this article helpful, please share it with others who may find it interesting.</p>
<p>Stay updated with my projects by following me on <a target="_blank" href="https://twitter.com/ijaydimples">Twitter</a>, <a target="_blank" href="https://www.linkedin.com/in/ijeoma-igboagu/">LinkedIn</a> and <a target="_blank" href="https://github.com/ijayhub">GitHub</a>.</p>
<h3 id="heading-related-articles">Related Articles</h3>
<ul>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/how-to-deploy-websites-and-applications/">How to Deploy Your Websites and Apps – User-Friendly Deployment Strategies</a></p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/backend-as-a-service-beginners-guide/">What is Backend as a Service (BaaS)? A Beginner's Guide</a></p>
</li>
<li><p><a target="_blank" href="https://dev.to/ijay/how-to-use-aws-s3-console-for-website-deployment-2mhh">How to Use AWS S3 Console for Website Deployment</a></p>
</li>
<li><p><a target="_blank" href="https://dev.to/ijay/how-to-secure-aws-infrastructure-1k8h">How to Secure AWS Infrastructure</a></p>
</li>
</ul>
<p>Thank you for reading.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy a Restful Web Service on Microsoft Azure App Service ]]>
                </title>
                <description>
                    <![CDATA[ Hey there! 👋, How’s it going? I hope you're doing well. Whether you're a seasoned coder, a curious learner, or just someone browsing through, welcome to my little corner of the internet. In this tutorial, we’ll dive into how you can deploy a web ser... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/deploy-a-restful-web-service-on-microsoft-azure-app-service/</link>
                <guid isPermaLink="false">67e6c180423cd4f90a6350b5</guid>
                
                    <category>
                        <![CDATA[ Azure ]]>
                    </category>
                
                    <category>
                        <![CDATA[ azure-devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud ]]>
                    </category>
                
                    <category>
                        <![CDATA[ REST API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Azure App Service ]]>
                    </category>
                
                    <category>
                        <![CDATA[ azure-app-services ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Alaran Ayobami ]]>
                </dc:creator>
                <pubDate>Fri, 28 Mar 2025 15:34:24 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743176028047/61eba7a3-5505-4152-9df5-59a1cb8c61ac.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Hey there! 👋, How’s it going?</p>
<p>I hope you're doing well. Whether you're a seasoned coder, a curious learner, or just someone browsing through, welcome to my little corner of the internet.</p>
<p>In this tutorial, we’ll dive into how you can deploy a web service on Microsoft Azure app service. It’s a topic I’ve been excited to explore, and I hope you’ll find it insightful and helpful. So grab a coffee, sit back, and let’s get started.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li><p>Basic understanding of RESTful web services</p>
</li>
<li><p>Programming knowledge</p>
</li>
<li><p>Docker basics</p>
</li>
<li><p>Docker Hub account</p>
</li>
<li><p>Command Line Interface (CLI) skills</p>
</li>
</ul>
<h3 id="heading-what-well-cover">What we’ll cover:</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-key-terminologies">Key Terminologies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-install-docker-and-docker-compose">How to Install Docker and Docker Compose</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-a-simple-get-client-info-web-service">How to Create a Simple Get Client Info Web Service</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-apps-docker-image">How to Build the App’s Docker Image</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-run-the-app-in-the-container">How to Run the App in the Container</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-an-azure-resource-group-on-azure-portal">How to Create an Azure Resource Group on Azure Portal.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-deploy-to-azure-app-service">How to Deploy to Azure App Service</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<p>Before we actually get started, I’d like to go over some key terminologies that I'll be using throughout this tutorial. Understanding these terms will make it easier to follow along and get the most out of what we're discussing. Let’s dive in!</p>
<h2 id="heading-key-terminologies">Key Terminologies</h2>
<h3 id="heading-1-docker"><strong>1. Docker</strong></h3>
<p>Docker is an open-source platform that simplifies application development, shipping, and deployment by using containers. With Docker, there are three key things you can do:</p>
<ul>
<li><p>build once and run anywhere</p>
</li>
<li><p>keep environment consistent, and</p>
</li>
<li><p>easily scale your application with container orchestration tools like Kubernetes.</p>
</li>
</ul>
<h3 id="heading-2-docker-hub"><strong>2. Docker Hub</strong></h3>
<p>Docker Hub is a cloud-based repository where developers can store, share, and manage Docker images. You can think of it as the GitHub for Docker images 😉.</p>
<h3 id="heading-3-docker-image"><strong>3. Docker Image</strong></h3>
<p>A Docker image is a lightweight<strong>,</strong> standalone, and executable package that includes everything needed to run a piece of software. You can also think of it as a blueprint for creating and running a container.</p>
<p>Once built, an image is immutable, meaning it cannot change after creation. Instead, you create a new version when updates are needed. This means that you’ll need to rebuild if you update the code that’s being run in the container.</p>
<h3 id="heading-4-container"><strong>4. Container</strong></h3>
<p>A container is a running instance of a Docker image. It provides an isolated environment where an application runs with all its dependencies, without interfering with the host system or other containers.</p>
<p>Wait, what?! <strong>😕</strong> Well, simply put, a container is like a tiny virtual machine that runs the built Docker image. But unlike traditional VMs, it has no init process (PID 1), meaning it always runs on a host system rather than being fully independent. By now, you should be getting the idea: no image, no container. A container depends on an image to exist. You have to cook before you serve, right? 🍽️</p>
<h3 id="heading-5-azure-resource-group"><strong>5. Azure Resource Group</strong></h3>
<p>An Azure resource group refers to a store or folder containing all related resources for an application you want to deploy to production using MS Azure Cloud. For example, if I want to deploy an eCommerce web app with MS Azure, I could create a resource group called EcommWebAppRG. I’d use this to create all the resources I needed for the web app to go live and be accessible – like VMs, DBs, caches, and other services.</p>
<p>Alrighty, now that all the terms are out of the way, lets get started with the tutorial so you can learn how to deploy a web service on Azure App Service.</p>
<p>Since we are deploying an app, let’s start by creating simple REST API app. I’ll be using Golang for the API development, but you can use any programming language/framework of your choice. If you’d like to follow along with the app for this tutorial, I’ve provided the source code <a target="_blank" href="https://github.com/Ayobami6/client_info_webservice">here</a>.</p>
<h2 id="heading-how-to-install-docker-and-docker-compose">How to Install Docker and Docker Compose</h2>
<p>To install Docker and Docker compose on your PC, for Mac and Windows you’ll need to download Docker desktop for your Operating System <a target="_blank" href="https://hub.docker.com/welcome">here</a>.</p>
<p>For Linux, you can run the following command:</p>
<pre><code class="lang-bash">sudo apt update <span class="hljs-comment"># update apt registry</span>
sudo apt install docker.io <span class="hljs-comment"># for docker engine</span>
sudo apt install docker-compose-plugin
</code></pre>
<p>After downloading the Docker desktop for your application, open your terminal and enter the following command to verify that Docker and Docker Compose have been successfully installed on your machine:</p>
<pre><code class="lang-bash">docker-compose -v <span class="hljs-comment"># this should show the version of docker compose cli you have on your PC</span>
</code></pre>
<p>My Docker Compose version is:</p>
<pre><code class="lang-bash">Docker Compose version v2.31.0-desktop.2
</code></pre>
<pre><code class="lang-bash">docker -v
</code></pre>
<p>You should see something similar if Docker has successfully been installed on your machine:</p>
<pre><code class="lang-bash">Docker version 27.4.0, build bde2b89
</code></pre>
<h2 id="heading-how-to-create-a-simple-get-client-info-web-service">How to Create a Simple Get Client Info Web Service</h2>
<p>Alright, I’ll use this handy tool called <code>sparky_generate</code> to generate the app code template. <code>sparky_generate</code> is a command line tool used to generate boilerplate code for backend development using various backend languages and frameworks.</p>
<p>Use this command to install and setup the tool on your Mac or Linux machine. If you are on Windows, you can use WSL.</p>
<pre><code class="lang-bash">wget https://raw.githubusercontent.com/Ayobami6/sparky_generate/refs/heads/main/install.sh
</code></pre>
<pre><code class="lang-bash">chmod 711 install.sh
./install.sh <span class="hljs-comment"># run the install command like so</span>
</code></pre>
<pre><code class="lang-bash">sparky <span class="hljs-comment"># run the sparky command like so</span>
</code></pre>
<p>You should see something similar to the below. Select whatever framework you want to use for your backend service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741520002226/b696a376-527b-412a-a8c7-967aca35217a.png" alt="Choose a project type in sparky" class="image--center mx-auto" width="1458" height="1408" loading="lazy"></p>
<p>I will select Go, because I’m using Go for this tutorial. You should have your project template ready once you’ve selected your framework. I won’t go through how to code the web service since it’s out of scope for this tutorial. The goal here is to deploy on Azure using Azure App Service.</p>
<h2 id="heading-how-to-build-the-apps-docker-image">How to Build the App’s Docker Image</h2>
<p>To build the Docker image for our app, we first need to create a Docker file that will include all the steps needed to build the image.</p>
<pre><code class="lang-bash">touch Dockerfile
</code></pre>
<p>We will be using a multistage Docker build to reduce the size of our Docker image. We need something as light as possible.</p>
<p>The multistage Docker build helps reduce size because we use different stages for different concerns. This helps ensure that only what’s needed runs the application in the final stage. For example, we might need certain artifacts to build the application, but we don’t need them to run the application. This is why we have the builder and runner stages in the Docker file below.</p>
<p>Our first stage (build) is concerned with building and bundling the application. The second stage (runner) is just used to run the executable we built in the first stage. So basically all files, modules, and so on used in the build process will be discarded in the runner since they’ve already served their purposes.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># using the first stage as build</span>
<span class="hljs-keyword">FROM</span> golang:<span class="hljs-number">1.23</span>-alpine AS build <span class="hljs-comment"># using alpine base image to reduce size</span>

<span class="hljs-comment"># install git on the machine</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apk add --no-cache git</span>

<span class="hljs-comment"># setting the current work directory on the vm to be /app</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># Copying all go dependency files to the working directory</span>
<span class="hljs-keyword">COPY</span><span class="bash"> go.mod go.sum ./</span>

<span class="hljs-comment"># Install go dependencies</span>
<span class="hljs-keyword">RUN</span><span class="bash"> go mod download</span>

<span class="hljs-comment"># copy all remaining files to the working directory</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-comment"># build the execuatable</span>

<span class="hljs-comment"># build the go program</span>
<span class="hljs-keyword">RUN</span><span class="bash"> go build -o api cmd/main.go</span>

<span class="hljs-comment"># stage 2 AKA the runner</span>
<span class="hljs-keyword">FROM</span> alpine:<span class="hljs-number">3.18</span>

<span class="hljs-keyword">RUN</span><span class="bash">  apk add --no-cache ca-certificates</span>

<span class="hljs-comment"># copy the go executable to the working directory</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=build /app/api .</span>

<span class="hljs-comment"># expose the service running port</span>
<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">6080</span>
</code></pre>
<p>This Dockerfile outlines all the steps for building a Docker image for our app. Lets go through all these steps line by line. Because we are using a multistage build here, our stages are <code>build</code> and <code>runner</code>.</p>
<ul>
<li><p>The first stage build starts from the first <code>FROM golang:1.23-alpine AS build</code>. It initializes a stage with all the steps in the stage. It states that the base image to be used for all the steps in this stage should use a pre-existing golang:1.23 image using an Alpine Linux distro to run all the steps in the stage.</p>
</li>
<li><p>Next, we have <code>RUN</code>, which is used to run the command <code>apk add git</code>, followed by setting the working directory to the /app folder.</p>
</li>
<li><p>Then we have the <code>COPY</code> command that copies all the Go dependencies that will run the Go program.</p>
</li>
<li><p>Following that is <code>RUN go mod download</code> which is used to install all the dependencies.</p>
</li>
<li><p>We then have the command <code>COPY . .</code> to copy all the files to the working directory /app of the image</p>
</li>
<li><p>Then the last step in the build stage, <code>RUN go build -o api cmd/main.go</code>, builds the app code main.go to an executable API. The second stage “runner“ uses an <code>alpine:3.18</code> base image, and also uses the <code>RUN</code> directive to add ca-certificates to the Alpine base image. The <code>COPY</code> is used here to copy just the built executable file from the builder image to the runner image.</p>
</li>
<li><p>We then expose port 6080 so our built container can accept an HTTP connection from port 6080.</p>
</li>
</ul>
<p>Now, let’s write our <code>docker-compose.yml</code> file to define and run our containerized service::</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>

  <span class="hljs-attr">client_info_app:</span>
    <span class="hljs-attr">build:</span> 
      <span class="hljs-attr">context:</span> <span class="hljs-string">.</span>
      <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">info_app</span>

    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"6080:6080"</span>

    <span class="hljs-attr">command:</span> <span class="hljs-string">"./api"</span>
</code></pre>
<h3 id="heading-understanding-the-yaml-structure">Understanding the YAML Structure</h3>
<p>YAML follows a key-value structure similar to a dictionary or hashmap. In our file, the top-level key is <code>services</code>, which acts as the parent key. Within <code>services</code>, we define individual service configurations.</p>
<p>In this case, we have a single service named <code>client_info_app</code>, which contains the necessary properties for Docker to build and run it.</p>
<ul>
<li><p><code>build</code>: This specifies how to build the Docker image for the service. The <code>context: .</code> tells Docker to look for the <code>Dockerfile</code> in the same directory as the <code>docker-compose.yml</code> file.</p>
</li>
<li><p><code>container_name</code>: This assigns a custom name (<code>info_app</code>) to the running container, making it easier to reference.</p>
</li>
<li><p><code>ports</code>: Defines the port mappings between the host and the container. The <code>-</code> before <code>"6080:6080"</code> indicates that multiple mappings could be listed. Here, port <code>6080</code> on the host is mapped to port <code>6080</code> inside the container.</p>
</li>
<li><p><code>command</code>: Specifies the command to execute inside the container once it starts. In this case, it runs <code>./api</code>.</p>
</li>
</ul>
<p>This structure allows us to define and configure multiple services within the <code>services</code> key if needed, but for now, we are only defining <code>client_info_app</code>.</p>
<h2 id="heading-how-to-run-the-app-in-the-container">How to Run the App in the Container</h2>
<p>You can use this command to start the container:</p>
<pre><code class="lang-bash">docker-compose up client_info_app
</code></pre>
<p>The above command will first build the image if it doesn’t exist. Then it runs it, because remember: <strong>no image, no container.</strong></p>
<p>If all looks good, you should have something similar to this, depending on your application framework:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741708248802/0c0e25a0-fa8c-4eea-adaa-5f4986b5fda6.png" alt="Go running webserver" class="image--center mx-auto" width="2424" height="658" loading="lazy"></p>
<p>You should also verify that your Docker image has been built as well. To do that, run the following:</p>
<pre><code class="lang-bash">docker images
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741708480967/dbb033eb-d336-4f22-934b-8d50333b6344.png" alt="Verify that Docker is successfully built" class="image--center mx-auto" width="2386" height="122" loading="lazy"></p>
<p>Voilà! You now have your Docker image built and ready for deployment on Azure App Service.</p>
<p>Let’s go on to the Azure Portal to deploy the app.</p>
<p><strong>Note:</strong> if you don’t have an active Azure Subscription, that’s fine – you can still follow along. If you want to get a trial account, you can <a target="_blank" href="https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account">get one here</a>.</p>
<h2 id="heading-how-to-create-an-azure-resource-group-on-azure-portal">How to Create an Azure Resource Group on Azure Portal.</h2>
<p>As a reminder, an Azure resource group refers to a store or folder containing all related resources for an application you want to deploy to production using MS Azure Cloud. Now let’s go about creating one.</p>
<p>When you login to the Azure portal, you should see some like this, the dashboard:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741709173044/1ab78dd4-97ac-43da-8599-d7b1d790eb13.png" alt="Azure dashboard" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p>Search for Resource group using the search bar:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741709263175/7946270b-11c6-4fef-9abc-996b1efc6f52.png" alt="Search for resource group" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741709619363/5cfce825-0af6-4d64-ae4e-e929ba03d919.png" alt="Resource group control pane" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p>Click on Create to create a new resource group:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741709715096/e519fe55-0ac3-4e9f-8b98-3f34cecfcd43.png" alt="Create a resource group" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p>Give your resource group a name and select a region for it. Here I will use the default East US 2, but you can use any you want. Then click on Review + create.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741770354937/253c33d2-d7b2-44b5-84df-9cb4977aec3b.png" alt="Review resource group creation" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741770386424/0a97c949-5c31-466b-be7f-a43f0783e125.png" alt="Resource group overview" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p>You should see something like the above after creating the resource group.</p>
<h2 id="heading-how-to-deploy-to-azure-app-service">How to Deploy to Azure App Service</h2>
<p>Ok now that we’ve created the resource group, let’s go ahead and create the Azure App Service. To do this, navigate back to the dashboard and click on Create a resource.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741772709338/2c353749-7e0a-42b0-894e-25b1f08cad15.png" alt="Azure dashboard" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741773327784/536358ff-241b-4ea1-876d-575abf12a27b.png" alt="Azure resources view" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p>You can search for Web App if you don’t see it in the list there. Then click on the Create Web App option:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741855971071/8c120d54-1df1-41f3-85b2-c03d9fd709f1.png" alt="Create a web app" class="image--center mx-auto" width="2398" height="1464" loading="lazy"></p>
<p>Here, select the resource group you created earlier, give the app a name, then select Container for the Publish option.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741882867790/bed19e70-726d-4696-b88f-754e36511f0a.png" alt="Web app creation configuration" class="image--center mx-auto" width="2384" height="1476" loading="lazy"></p>
<p>Before you click on Create, go back to your dev workspace (VS Code or whatever IDE you are using) to push your Docker image to Docker Hub so you can add it there before proceeding to the next steps.</p>
<p>But why do you need to push to Docker Hub? Well, first of all, for accessibility – so we can easily share it with others or have other services access it (which is what we need here).</p>
<p>Remember how I compared Docker Hub with Github earlier? Docker Hub helps you host your Docker image on the internet and make it available for others or for various services on the internet to access if you make it public. Otherwise it’s limited to only authorized services.</p>
<p>To push the Docker image to Docker Hub, you first need to tag the Docker image with your Docker Hub username. Go to Docker <a target="_blank" href="https://hub.docker.com/">Docker Hub</a> to register and get your username if you don’t have one.</p>
<p>Run the following:</p>
<pre><code class="lang-bash">docker tag client_info_webservice-client_info_app:latest ayobami6/client_info_webservice-client_info_app:latest
<span class="hljs-comment"># this adds the latest tag to your docker image</span>
</code></pre>
<p>This shows that you successfully tagged your image with the latest tag.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741941521765/f624e0d1-2380-41cc-8bbf-f05e55d0b0ad.png" alt="Tagged Docker images" class="image--center mx-auto" width="2340" height="78" loading="lazy"></p>
<p>Before you actually push the image to Docker Hub, go ahead and login to it with the Docker CLI.</p>
<pre><code class="lang-bash">docker login <span class="hljs-comment"># this will prompt for browser authetication, for the prompt and login your account with your usernam and password</span>
</code></pre>
<p>Push the image to Docker Hub like this:</p>
<pre><code class="lang-bash">docker push ayobami6/client_info_webservice-client_info_app:latest
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741941723747/5947d1eb-82fe-4bf3-a0d7-e20bbc9d7de4.png" alt="Push Docker image to Docker Hub" class="image--center mx-auto" width="2358" height="130" loading="lazy"></p>
<p>You should see something like the below once you enter the push command on Docker Hub.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741941947561/7050ed8b-b7bf-4e07-91a6-765549b5d715.png" alt="Verify Docker image on Docker Hub" class="image--center mx-auto" width="2146" height="544" loading="lazy"></p>
<p>Alright, now that you have the Docker image on Docker Hub, you can go ahead and deploy it using Microsoft Azure App Service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741944017329/32080357-e214-4864-b5cb-0fc8ad77a71f.png" alt="Create web container selection for publish" class="image--right mx-auto mr-0" width="2448" height="1722" loading="lazy"></p>
<p>Click on the container on the top menu bar to configure the container settings.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741944418747/6f9092e9-e6fc-4098-86d0-ba79eda37a58.png" alt="Container section of Azure web app" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Here, select Other container registries.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741944507676/d4c3d17e-e3fa-4c43-99b5-b768fd900309.png" alt="Other registry selection for Azure web app" class="image--center mx-auto" width="2418" height="1634" loading="lazy"></p>
<p>Select public, because your pushed image is public (meaning it’s publicly accessible over the internet).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743071959687/48cc5a78-4b72-4c8f-9ffb-d32c065e1a63.jpeg" alt="Docker image access type selection" class="image--center mx-auto" width="1080" height="729" loading="lazy"></p>
<p>Add your Docker image and tag. You can get this when you run the command <code>docker images</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743072606918/d17f7a31-bee3-41b9-a685-d7de0c0f6b39.jpeg" alt="Image and tag selection" class="image--center mx-auto" width="1080" height="729" loading="lazy"></p>
<pre><code class="lang-bash">λ MACs-MacBook-Pro ~ → docker images
REPOSITORY                                        TAG         IMAGE ID       CREATED         SIZE
ayobami6/client_info_webservice-client_info_app   latest      a14f2a5b3bd4   2 weeks ago     30.8MB
postgres                                          13-alpine   236985828131   4 weeks ago     383MB
glint_pm_frontend-nextjs                          latest      424233ceaa4b   4 weeks ago     1.72GB
flask_app-flask_app                               latest      ff6ecfc4ba5a   5 weeks ago     203MB
nginx                                             latest      124b44bfc9cc   7 weeks ago     279MB
encoredotdev/postgres                             15          58b55b0e1fc7   10 months ago   878MB
λ MACs-MacBook-Pro ~ →
</code></pre>
<p>Copy the repository name and tag – in my case I have <code>ayobami6/client_info_webservice-client_info_app</code> and tag <code>latest</code> → <code>ayobami6/client_info_webservice-client_info_app:latest</code>.</p>
<p>Then add your startup command. If you are not using Go for the development like I am, your startup command will be different – so just use the command you added to your Docker compose command key, like so <code>command: "./api"</code>. Copy just the value (mine is .<code>/api</code>) don’t add the double quotes, and add it to the startup command.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743072910308/a208c0a1-718b-4725-99b5-47bf33691fca.jpeg" alt="Add startup command to the web app container configuration" class="image--center mx-auto" width="1080" height="729" loading="lazy"></p>
<p>This start up command is the command that will start the application from the container and get the container running.</p>
<p>Click on review and create to create the service.</p>
<p>Once the deployment is complete you’ll be redirected here:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741944994364/31e051a6-2da1-46fa-a748-c2bdd20528be.png" alt="Web app overview" class="image--center mx-auto" width="2880" height="1634" loading="lazy"></p>
<p>Congratulations! You’ve successfully deployed your web service to Azure App Service. You can visit your app using the default domain from the overview. Mine is <a target="_blank" href="http://clientinfoapp-hdgcdmgjdyd5ecfy.canadacentral-01.azurewebsites.net">here</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741945313388/19c73507-0b48-49ef-9885-515784cb0f9b.png" alt="Deployed service test on Postman" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741945461019/30a668c7-0552-4905-bbb0-c9db0b587387.png" alt="Deployed service test on web browser" class="image--center mx-auto" width="1674" height="1514" loading="lazy"></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Deploying a RESTful web service on Microsoft Azure App Service is a powerful way to leverage cloud technology for scalable and efficient application hosting.</p>
<p>Understanding key terms like Docker, Docker Hub, and Azure Resource Groups will help you streamline the deployment process.</p>
<p>This guide walked you through creating a simple web service, building a Docker image, and deploying it on Azure. By following these steps, you can confidently deploy your applications, ensuring they are accessible and performant.</p>
<p>Thank you for following along, and I hope this tutorial has been insightful and helpful in your cloud deployment journey. If you found it useful, feel free to share it with others who might benefit from it. Happy coding and deploying!</p>
<p>Stay tuned for more insightful content, and let's continue learning and growing together. Cheers to building smarter, more efficient solutions with Azure.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy Your Project On Vercel With A Custom Domain ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever built a project but found it difficult to make it live on the internet? Well, worry no more because this article will help you do that. In this article, I will introduce you to one of the fastest and easiest deployment platforms for bri... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-deploy-your-project-on-vercel/</link>
                <guid isPermaLink="false">671bf3148df28644ca9012f9</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Okoro Emmanuel Nzube ]]>
                </dc:creator>
                <pubDate>Fri, 25 Oct 2024 19:35:48 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729806244084/f4aca70a-801e-4577-8073-e078323db51a.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever built a project but found it difficult to make it live on the internet? Well, worry no more because this article will help you do that.</p>
<p>In this article, I will introduce you to one of the fastest and easiest deployment platforms for bringing your code/project to the web.</p>
<p>I will also show you how you can deploy a web application with your custom domain and why it’s important to do so.</p>
<p>Let's dive right in.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-overview-of-vercel">Overview of Vercel</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-domains">What are Domains?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-a-vercel-account">How to Set Up a Vercel Account</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-configure-a-custom-domain">How to Configure a Custom Domain</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-overview-of-vercel"><strong>Overview of Vercel</strong></h2>
<p>Vercel is a cloud hosting platform that provides tools that enable developers to build, deploy and scale their web applications. One important fact about this platform is that it is quick, easy to navigate/use, and it is very efficient.</p>
<p>Vercel supports and deploys many frameworks with minimal configuration, especially frameworks built with JavaScript. Here is a list of frameworks that can be deployed to Vercel: Angular, Astro, Brunch, React, Dojo, Gatsby.js, Next.js, Nuxt.js, Vite, Vue.js, Vuepress, and so on.</p>
<p>You may not know all these frameworks at this point, but it's important to know that Vercel supports these frameworks and many more.</p>
<p>See <a target="_blank" href="https://vercel.com/docs/frameworks/more-frameworks">Vercel documentation</a> to view more.</p>
<h2 id="heading-what-are-domains"><strong>What are Domains?</strong></h2>
<p>Domains are ‘unique identifiers’ used to locate or find a specific website on the internet. For example, <a target="_blank" href="http://freecodeCamp.org">freecodeCamp.org</a>. Whenever you see a domain name, you should have an idea of the website it belongs to.</p>
<p>It is important to note that a domain name can be created with a combination of letters (A-z), numbers (0-9), and dash characters.</p>
<h3 id="heading-importance-of-having-a-domain-name"><strong>Importance of Having a Domain Name</strong></h3>
<p>Here are some reasons why you should have a domain name:</p>
<ul>
<li><p><strong>Easy to find and locate you:</strong> Having a domain name makes the work of finding your business easy and also increases the possibility of making more sales from your web page. If you have a physical store, not everyone may want to visit it.</p>
</li>
<li><p><strong>For Professionalism and Credibility:</strong> Imagine having a business but no website or domain. This will make you look unprofessional to customers because they may not take you seriously.</p>
</li>
<li><p><strong>Increase your Online Presence:</strong> With a domain name, your online presence or your brand’s online presence is increased, which makes you more visible.</p>
</li>
</ul>
<p>Now let's talk about setting up a Vercel account. This process is actually simple and easy to follow.</p>
<h2 id="heading-how-to-set-up-a-vercel-account"><strong>How to Set Up a Vercel Account</strong></h2>
<p>The first step is to visit <a target="_blank" href="https://vercel.com/">Vercel</a>.</p>
<p>On the Vercel home page, click on the sign-up button, located at the top right corner of the home page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729808253459/5946836c-3de9-4d27-bb5a-dd992446bd9c.png" alt="Vercel Home page" class="image--center mx-auto" width="1806" height="711" loading="lazy"></p>
<p>Choose your "Plan Type" and then input your name. Then click on Continue to proceed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729807310680/e4872073-f8de-4e74-9555-281cf96efa01.png" alt="Vercel setup process " class="image--center mx-auto" width="839" height="739" loading="lazy"></p>
<p>Next, connect the account where you will be importing your project from. You’ll be provided with three options: GitHub, GitLab, or BitBucket.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729807383749/7bab422e-87cd-4b9a-b254-25af80f9a73b.png" alt="Linking your Github or GitLab or Bitbucket to Vercel" class="image--center mx-auto" width="708" height="595" loading="lazy"></p>
<p>In my case, I clicked “Continue with GitHub”.</p>
<p><strong>Note</strong>: If you don’t have a GitHub account, you can read see how to create one <a target="_blank" href="https://www.freecodecamp.org/news/git-and-github-the-basics/">here</a>.</p>
<p>Once you have linked your GitHub account with Vercel, the interface should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729807623924/c44c31f2-e77a-4538-9a86-c80d06680779.png" alt="Vercel setup complete interface" class="image--center mx-auto" width="1605" height="702" loading="lazy"></p>
<p>At this point, you’re done setting up your Vercel account. The next step is to deploy your project.</p>
<p>From the image description above, you can see an “import” button. Go to the particular project you want to deploy and click on the import button. In my case, I named my project repository “practice-purpose”.</p>
<p>Once you click the import button, it should lead you to the next page where you can input your project name and finally deploy it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729807744450/3eaf81e5-f53c-45bb-9890-166516ff17c4.png" alt="Deployment setup interface" class="image--center mx-auto" width="849" height="801" loading="lazy"></p>
<p>Wait for a few minutes for the deployment to be complete. After that, your interface should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729807863920/fcfdfaa7-a42e-451a-b539-74dc151eca69.png" alt="After Deployment Interface" class="image--center mx-auto" width="1458" height="808" loading="lazy"></p>
<p>Congratulations! At this point, you have deployed your first project.</p>
<h2 id="heading-how-to-configure-a-custom-domain"><strong>How to Configure a Custom Domain</strong></h2>
<p>Before you continue with the process of configuring a custom domain, you must have gotten your domain ready. If not, I have added a quick video that will guide you.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/JRRXTR7PUug" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<p>For this article, I will be making use of Namecheap, because that is where I got the domain I am using. Feel free to use any domain provider of your choice.</p>
<p>At this point, your domain name should be available. If that is so, let’s continue.</p>
<p>In your deployment page on Vercel, click on “Domain” in the navigation bar, input your custom domain name (the one you bought) in the space provided and click on “Add”.</p>
<p>When you click on the “Add” button, a prompt should pops up – don’t change anything, use the recommended option and click on the ‘Add’ button.</p>
<p>For the next step, click on the “Nameservers” option, then click on “Enable vercel DNS”.</p>
<p>Lastly, copy the DNS and head to the website where you purchased your domain.</p>
<p>Here are detailed images that show the above steps.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729808085843/7061315b-ca1e-4201-bd00-6d3b02bfa456.gif" alt="Custom Domain Setup on Vercel" class="image--center mx-auto" width="1468" height="771" loading="lazy"></p>
<p>At this point, all you have to do is link your custom domain with Vercel, using the DNS you just copied from Vercel.</p>
<p>Here is how you can go do that (I’ll use the Namecheap process to explain it):</p>
<p>Go to the website where you purchased your domain and log in to your dashboard.</p>
<p>Head to the domain list option and click on it. This should display a list of domains you have purchased with that account.</p>
<p>Next, head to the domain name you used for your Vercel project and click on the “manage” option. This should open a page where details of the domain is displayed.</p>
<p>Find where you have “nameservers” and choose the “custom DNS” option. Lastly, paste each of the DNS you copied from Vercel into the spaces provided and click on save.</p>
<p>After this, you should get a prompt stating that the custom domain will be live in the next 48 hours. In most cases, it doesn’t take up to a day for it to be active.</p>
<p>Here is a detailed image that shows the above steps.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729808142674/7d533d7d-0ea9-4cf8-bd19-183283ffa150.gif" alt="Custom Domain Setup on Namecheap" class="image--center mx-auto" width="1890" height="981" loading="lazy"></p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this article, you learned about the Vercel platform, what a domain name means and its importance.</p>
<p>You also learned how to set up your Vercel account, deploy your project, and add a custom domain.</p>
<p>If you’ve read up to this point, I want to say a very big congratulations, and I hope you got the value out of this article.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
