From Flask to Gunicorn + Nginx: Improving Backend Performance and Stability
Recently, I’ve been facing a recurring issue: after running for a while, my backend server becomes unresponsive.
Restarting the process temporarily fixes it, but this is a serious problem once the user base grows.
One night before going to bed, I opened my app and, once again, the backend was unresponsive. I decided to fix it before
sleeping. Since I had no prior experience in this area, I asked ChatGPT for help. It first suggested checking the logs
for errors, but I found nothing—no errors, not even records of service calls.
Then, I showed AI my app.py file, and it identified the root cause: I was running Flask with its built-in development
server (app.run(...)), without configuring Nginx as a reverse proxy. This meant the frontend was directly connecting to
port 5000. The development server is single-process and single-threaded, so whenever a slow I/O operation occurred, the
only worker thread would get blocked, causing the service to freeze.
Although this didn’t immediately solve the issue, I realized I would eventually need a production-ready setup. So, with
AI’s help, I replaced my app.py and backend routes to run under Gunicorn, enabling multiple processes and threads. Next,
I configured Nginx as a reverse proxy to handle all routes. Finally, I updated my frontend JavaScript requests from
https://
Fix Logging Issues
After switching my backend to Gunicorn, stability improved a lot. However, I ran into a development-time issue: inside my route handlers I used print to write logs, and I started the app with this command to redirect all stdout/stderr to a file:
exec python3 src/backend/app.py > ./log/app.out 2>&1
In theory, everything printed should land in app.out, but nothing showed up. A quick search told me this is pretty common. I asked ChatGPT how to log “properly,” and it suggested using Python’s logging library.
So at the top of app.py I added:
import logging
And also, I add:
handler = RotatingFileHandler("./log/app.out", maxBytes=5_000_000, backupCount=3, encoding="utf-8")
With that in place, I can call logger in my route functions and use levels like info and warning to make searching easier. Now, checking logs is much smoother.
Compatible with Android devices
Today, I successfully adapted my app to the Android system. You can click here to view the source code. Since my app is essentially a web page, the adaptation was relatively simple — it only required opening a link through the app. However, as I had never worked with Android development before, the process still felt quite challenging. Compared to Apple’s development, Android involves many more configuration items, which at times left me overwhelmed. Fortunately, with the help of AI, I was able to complete it. But when I tried to add a splash screen, I unexpectedly discovered that Android does not support playing videos with an alpha channel. This means I couldn’t create the same startup animation as on iOS. In the end, I decided to skip the splash animation altogether.
Adapting to App Store Guidelines with Capacitor
Recently, while reviewing Apple’s App Store guidelines, I realized that purely wrapping an H5 web app inside a shell makes it impossible to pass the review. Some native functionalities are required, such as notifications, widgets, or integrations with Apple Health. Since my current H5 frontend could not achieve this, I started exploring new solutions. That’s when I discovered Capacitor, a framework that can package JavaScript-based apps into cross‑platform native applications, while also providing a rich plugin system for interacting with native features. I also learned there is a UI library named Ionic , which automatically adapts to iOS and Android design languages. Initially, I tried migrating my existing UI into Ionic, but the results were unsatisfactory, so I abandoned that path. Instead, I use Capacitor only. After installation, I could control app behavior directly from the terminal and use plugins like vibration, which worked seamlessly across both iOS and Android. Soon, I added vibration feedback to most buttons, greatly enhancing user immersion. With this framework, adapting system‑level features and accessing Apple Health data became much more approachable, and I finally felt confident about aligning my app with App Store requirements. You can see my app with capacitor over here.
Building a Medication Reminder Feature
Over the past few days, I updated quite a few features, such as the mood selection interface and the record page. These updates didn’t carry much record‑keeping value; they were mostly about UI tweaks and refining the interaction with AI, which handled them excellently. After that, I decided to build a medication reminder feature. Since it was logically independent and didn’t rely on a database, I thought it would be quick to implement — though it turned out more complex than expected. Capacitor made it easy to support iOS and Android push notifications via plugins, but the web version posed challenges. In the end, I removed the feature on web and guided users to download the app if they needed reminders. I completed the environment judgment with the following code:
function isCapacitorApp() {
try {
// Check if the Capacitor object exists
if (typeof window.Capacitor === 'undefined') {
return false;
}
// Check if it is a native platform
if (typeof window.Capacitor.isNativePlatform === 'function') {
return window.Capacitor.isNativePlatform();
}
// Fallback check: verify the basic structure of the Capacitor object
return !!(window.Capacitor && window.Capacitor.Plugins);
} catch (error) {
console.warn('Error while detecting Capacitor environment:', error);
return false;
}
}
The basic notification sending worked quickly, but to make it more useful, I added recurring reminders like daily and weekly schedules. While AI generated much of the code, I struggled to understand the overall structure. A bug appeared: switching pages caused duplicate notifications and reminder cards wouldn’t clear. AI failed to fix it despite many attempts, so I dug into the code myself. Eventually I found that reloading index.js
triggered the duplicate requests. After I reported this root cause, AI instantly gave a working fix: assigning each reminder a random ID that can only trigger once.
This solved the bug neatly. Later, I polished the feature further with extra options. Through this process I realized that while AI can write code very efficiently, its context limitations prevent it from fully grasping project architecture. A skilled programmer’s role is still crucial — to understand the logic, direct the development, and collaborate with AI for maximum efficiency.
The process of making the app is not over yet, I will continue to update this passage, thank you, Because this post is already quite long, I will open a separate page for the next post. You can click here to view the next article.