Introduction 🐳
Hello from Japan! 🇯🇵
I am a professional truck driver teaching myself Python while working toward a career transition into web engineering.
After 113 cumulative hours of Python study, I reached the stage of preparing my Flask applications for production.
Before this deployment, I had already conducted several hands-on tests in my local environment, including:
- Dockerization
- Data persistence
- Container service-name resolution
- Logging design
- Flask and PostgreSQL multi-container operation
This time, I finally deployed two applications to Render and built a workflow in which every push to GitHub automatically triggers a new build and deployment.
In other words, I created a basic CI/CD pipeline.
The applications can be accessed directly through a browser.
🍞 Bakery Sales Management System
Technology stack: Flask + PostgreSQL
https://bakery-salesdata.onrender.com/
View the sales_data_app repository on GitHub
🕊️ puoppo Public Opinion Analysis System
Technology stack: Flask + SQLite + RSS + Gemini API
View the puoppo_app repository on GitHub
Note about free-tier startup behavior
These applications run on a free hosting plan.
If they have not been accessed for a while, the server may need to wake from a sleeping state.
During startup, a temporary black screen with white deployment or startup logs may appear.
Please wait for the startup process to finish. The application page should appear afterward.
The exact startup time varies depending on the environment, so I have intentionally not listed a specific number of seconds.
This article records the events that actually occurred while building this deployment workflow.
I have avoided exaggeration and focused on the observable facts.
📦 1. Why the Production Deployment Went Smoothly
The Render deployment itself was completed without any major trouble.
The main reason was that I had already completed several important tests locally before beginning the production deployment.
These included:
- Testing data persistence with Docker volumes
- Testing service-name resolution between containers
- Designing logs for standard output using Python's
loggingmodule - Verifying container lifecycles
- Confirming the relationship between the Flask application and PostgreSQL
Through these tests, I had already established answers to two important questions:
How should containers be designed so that deleting them does not cause a problem?
and:
What information should I use to identify the cause of an error?
Because that foundation was already in place, the Render-side work was mostly limited to:
- Connecting the GitHub repository
- Configuring environment variables
- Selecting the build and startup settings
- Confirming the deployment logs
As a result, unexpected behavior was kept to a minimum.
The deployment was not smooth because production deployment is always easy.
It was smooth because much of the uncertainty had already been removed in the local environment.
🚧 2. The Deployment Pipeline Stopped on the First Line
The deployment of bakery_salesdata completed without any major issue.
However, the Render deployment of puoppo stopped immediately after the build process.
This happened even though I had already completed the local testing.
Actual Error Log
Traceback (most recent call last):
File "/app/app.py", line 1, in <module>
from dotenv import load_dotenv
ModuleNotFoundError: No module named 'dotenv'
==> Exited with status 1 while running your code.
Identifying the Cause
puoppo uses the Gemini API.
To load GEMINI_API_KEY from a .env file in the local environment, the application imports python-dotenv.
However, python-dotenv was missing from requirements.txt.
In my local environment, the package had already been installed during an earlier experiment.
It remained inside the virtual environment, so the application started normally.
Render behaved differently.
Render created a clean environment and installed only the dependencies explicitly listed in requirements.txt.
Because python-dotenv was not included, the missing dependency was exposed immediately when the first line of app.py was executed.
Applying the Fix
I added python-dotenv to requirements.txt, committed the change, and pushed it to GitHub.
git commit -m "fix: add python-dotenv to requirements.txt"
The GitHub push triggered a new Render build automatically.
Analysis of the Result
This incident demonstrated an important fact:
A successful local execution does not prove that the application has declared all of its dependencies correctly.
The local environment had hidden the missing dependency because it contained packages installed during previous development work.
The clean production environment exposed the mistake.
However, this failure also showed that the deployment process stopped safely.
The application was not left running in a partially broken state.
The missing dependency was detected before the application became available.
In that sense, the deployment process behaved according to the Fail-Fast principle.
The system identified the configuration problem early and stopped immediately.
⚓ 3. Choosing Not to Pursue Full Data Persistence
puoppo uses SQLite as its database.
For this Render deployment, I decided not to migrate it to PostgreSQL.
I kept SQLite.
Background of the Decision
On Render's free plan, local disk storage is ephemeral.
This means that an SQLite database file may be reset when the service is redeployed or restarted.
For bakery_salesdata, this limitation was unacceptable because the application stores sales records that must remain available.
For that reason, I migrated that application to PostgreSQL.
However, puoppo has a different purpose.
Its main functions are:
- Retrieve current articles from RSS feeds
- Send the collected information to the Gemini API
- Generate summaries and sentiment or trend analysis
The displayed information is based on current RSS articles.
Long-term accumulation of historical data is not essential to the core demonstration.
Analysis of the Decision
For puoppo, I prioritized demonstrating:
- Stable application startup
- RSS retrieval
- Gemini API integration
- AI-generated summaries
- Browser-based accessibility
I therefore decided not to spend additional time and resources on full database persistence.
This was not an accidental omission.
It was a design decision based on the role of the application.
Instead of applying the strongest possible architecture to every feature, I worked backward from the application's purpose and allocated resources only where they were necessary.
In logistics terms, I did not load the truck with equipment that the delivery did not require.
🔄 4. Building the Automatic Deployment Workflow
After connecting the GitHub repositories to Render, the deployment flow became:
Edit the code locally
↓
Commit the changes
↓
Push to GitHub
↓
Render detects the push
↓
Render builds the application
↓
Render deploys the new version
This removed the need to manually upload application files after every change.
For example, when I fixed the missing python-dotenv dependency, the process was:
git add requirements.txt
git commit -m "fix: add python-dotenv to requirements.txt"
git push
After the push, Render automatically started a new build.
This created a simple CI/CD workflow:
- GitHub became the source of truth
- Render watched the repository
- Every push triggered a new build
- Successful builds were automatically deployed
- Failed builds stopped before publication
This workflow also made deployment history easier to understand.
Each deployed version corresponded to a Git commit.
🔍 5. The Real Cause of “It Worked Locally”
The phrase:
It worked on my machine.
often sounds like a mysterious environment problem.
In this case, the cause was specific and measurable.
The local environment and the production environment did not contain the same dependencies.
My local virtual environment contained python-dotenv.
The production environment installed only what was listed in requirements.txt.
The difference was not caused by Flask or Render behaving randomly.
It was caused by an undeclared dependency.
The actual structure was:
Local environment:
python-dotenv was already installed
↓
The application started successfully
↓
The missing requirements entry remained hidden
Production environment:
Only requirements.txt was installed
↓
python-dotenv was missing
↓
The application stopped immediately
This experience changed the meaning of “works locally” for me.
It does not mean:
The application is ready for production.
It only means:
The application works under the current local conditions.
To make that result reproducible, the conditions themselves must also be documented and recreated.
That includes:
- Python version
- Dependencies
- Environment variables
- Database configuration
- Startup commands
- External services
- Storage behavior
🛡️ 6. How Previous Local Testing Helped in Production
The local tests I had completed earlier directly supported this deployment.
Container Lifecycle Testing
I already understood that containers should be treated as disposable runtime units.
This reduced the fear of rebuilding or recreating the environment.
Data Persistence Testing
I had verified that PostgreSQL records remained in a named volume after container recreation.
That knowledge made it easier to separate application deployment from database persistence.
Service-Name Resolution Testing
I had confirmed that Docker Compose service names could be used instead of hard-coded IP addresses.
This helped me understand environment-independent service communication.
stdout Logging
I had already configured application logs to be sent to standard output.
That made Render's deployment and runtime logs useful for troubleshooting.
Fail-Fast Checks
I had introduced startup checks for required environment variables and application configuration.
This reduced the chance of publishing an application that appeared to start but failed only after user interaction.
These tests did not eliminate every production problem.
They made the remaining problems easier to identify.
🚩 Conclusion
I deployed two Flask applications to Render and created an automatic deployment workflow connected to GitHub.
The applications were:
bakery_salesdatapuoppo
With the new workflow, a GitHub push automatically triggers a build and deployment.
The local testing I had already completed—covering container lifecycles, persistent storage, service-name resolution, and logging—provided a reliable foundation for the production deployment.
At the same time, the deployment exposed a problem that had remained hidden locally:
-
python-dotenvwas installed locally - It was missing from
requirements.txt - Render created a clean environment
- The application stopped with
ModuleNotFoundError - The missing dependency was added
- A new push triggered an automatic redeployment
This incident demonstrated both the danger of relying on local success and the value of Fail-Fast behavior.
I also chose to keep SQLite in puoppo, even though the free hosting environment uses ephemeral storage.
That decision was based on the purpose of the application.
Because puoppo analyzes current RSS content, long-term database persistence was not essential to the demonstration.
The final architecture therefore differed between the two applications:
| Application | Database | Persistence Strategy | Main Purpose |
|---|---|---|---|
bakery_salesdata |
PostgreSQL | Persistent database storage | Store and manage sales records |
puoppo |
SQLite | Ephemeral demo storage | Demonstrate RSS and Gemini API analysis |
The most important lesson was this:
A production deployment should not be based on assumptions about what will probably work.
It should be based on observable behavior, declared dependencies, reproducible environments, and logs that reveal what the system is actually doing.
The applications are currently available here:
🍞 Bakery Sales Management System
https://bakery-salesdata.onrender.com/
🕊️ puoppo
By continuing to record actual failures and actual fixes instead of relying on assumptions, I believe I can build a more reliable foundation for keeping applications running in production.

Top comments (0)