Recipe App
Save recipes, ingredients, notes, ratings, photos, and cooking changes.
A good next project is a simple ____ App. Replace the blank with something you actually care about: Recipe App, Seed App, Movie App, Music App, Book App, Tool App, or anything else you want to organize.
My real example is the Seed App hosted on LaunchShell.com. It started as a practical way to track seeds, trays, grow bags, planting dates, weather, and garden notes.
This is a medium-hardness project. It is doable on a free-tier server or a cheap VPS around $5 per month. I use Hetzner for this kind of practical Linux VPS work, but the same pattern applies on other providers.
The first app does not need accounts, payments, comments, AI, or a complicated database. It just needs to solve one real problem for me.
Save recipes, ingredients, notes, ratings, photos, and cooking changes.
Track seeds, trays, grow bags, planting dates, sprouting, and weather.
Track movie files, size, genre, watched status, backup priority, and drive space.
Track albums, artists, formats, tags, missing artwork, and files to clean up.
This is where a cloud server turns into a real portfolio project.
I create routes, render HTML with Jinja templates, serve static CSS and images, and handle forms.
Instead of starting with a database, I store records in JSON files so the data is easy to inspect, copy, back up, and move.
I run the app with Gunicorn, keep it alive with systemd, put Apache in front, point DNS at it, and add HTTPS.
This is the whole project path in one list.
data/ before moving to a database later.This is the app-building part before the real deployment setup.
I chose one small thing to organize. The best beginner app is personal and boring in a useful way.
Before writing routes, I wrote one example JSON item. That kept the project from turning into chaos.
{
"id": "example-001",
"title": "Example Item",
"category": "Personal",
"notes": "What I want to remember",
"rating": 5,
"tags": ["favorite", "test"]
}
This layout is enough for a first real Flask app.
blank-app/
├── app.py
├── requirements.txt
├── data/
│ └── items.json
├── static/
│ └── style.css
└── templates/
├── index.html
├── detail.html
└── form.html
A virtual environment keeps this app's Python packages separate from the rest of the system.
cd blank-app
python3 -m venv .venv
source .venv/bin/activate
pip install Flask gunicorn
pip freeze > requirements.txt
The first version only needs to load JSON and show it in the browser.
from flask import Flask, render_template
import json
import os
app = Flask(__name__)
DATA_FILE = os.path.join(app.root_path, "data", "items.json")
def load_items():
with open(DATA_FILE) as f:
return json.load(f)
@app.route("/")
def index():
return render_template("index.html", items=load_items())
if __name__ == "__main__":
app.run(debug=True)
I tested the app before touching Apache, DNS, HTTPS, or systemd.
source .venv/bin/activate
flask --app app run
Then I opened http://127.0.0.1:5000 in the browser.
The order matters. First show data. Then add detail pages. Then add create/edit forms.
For a personal app, I do not want the whole internet editing my recipes, movies, music, or seed collection.
The password should come from an environment variable, not from code committed to GitHub.
APP_PASSWORD=change-this
SECRET_KEY=generate-a-long-random-value
This is the part that makes it feel like a real web app instead of a local Python script.
On the cloud server, I installed Python tools, Apache, Git, and basic security packages.
sudo apt update
sudo apt install -y python3-venv python3-pip apache2 git ufw fail2ban
The cleanest version is to push the project to GitHub, then clone it on the server.
mkdir -p ~/apps
cd ~/apps
git clone https://github.com/YOURNAME/blank-app.git
cd blank-app
The server gets its own virtual environment.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Gunicorn runs the Flask app as a real application server. I bound it to localhost so it was not directly exposed to the internet.
.venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app
In another SSH window, I tested:
curl http://127.0.0.1:5000
Secrets do not belong in the Git repository.
sudo nano /etc/blank-app.env
SECRET_KEY=replace-with-a-long-random-string
APP_PASSWORD=replace-with-a-real-password
SESSION_COOKIE_SECURE=1
systemd keeps the app running, starts it on boot, and gives me logs through journalctl.
sudo nano /etc/systemd/system/blank-app.service
[Unit]
Description=Blank Flask App
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/apps/blank-app
EnvironmentFile=/etc/blank-app.env
ExecStart=/home/ubuntu/apps/blank-app/.venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app
Restart=always
[Install]
WantedBy=multi-user.target
This starts the app and makes it come back after reboot.
sudo systemctl daemon-reload
sudo systemctl enable --now blank-app
sudo systemctl status blank-app --no-pager
Apache handles public web traffic. Flask stays private on 127.0.0.1:5000.
sudo a2enmod proxy proxy_http headers
sudo nano /etc/apache2/sites-available/blank-app.conf
<VirtualHost *:80>
ServerName app.example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ErrorLog ${APACHE_LOG_DIR}/blank-app-error.log
CustomLog ${APACHE_LOG_DIR}/blank-app-access.log combined
</VirtualHost>
Then I tested Apache before reloading it.
sudo a2ensite blank-app.conf
sudo apachectl configtest
sudo systemctl reload apache2
I created an A record for the app subdomain.
Type: A
Name: app
Value: YOUR_SERVER_PUBLIC_IP
Proxy: on, if using Cloudflare for web traffic
Once the domain reached the server over HTTP, I used Certbot.
sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d app.example.com
A web app is not finished until I know how to troubleshoot it and back it up.
sudo journalctl -u blank-app -f
sudo tail -f /var/log/apache2/blank-app-error.log
tar -czf ~/blank-app-data-$(date +%F).tar.gz ~/apps/blank-app/data
This is the reason I would call it a ____ App guide instead of only a Seed App guide.
| Part | Generic version | Examples |
|---|---|---|
| Item name | item |
recipe, seed, movie, album, book, tool |
| Data folder | data/items.json or data/items/ |
data/recipes/, data/movies/, data/albums/ |
| List page | Show all items | Recipe list, movie library, seed catalog, music collection |
| Detail page | Show one item | One recipe, one movie, one album, one seed variety |
| Form | Add/edit item | Add recipe notes, update movie drive, edit seed planting info |
| Deployment | Same every time | Gunicorn, systemd, Apache, DNS, HTTPS |
This is a good student project, personal tool, or portfolio demo. It is not a full production platform.
Recipe trackers, small collections, lab dashboards, planting notes, file inventories, and simple internal tools.
Do not turn this first version into a public multi-user app with sensitive data. That needs stronger authentication and a real security review.
JSON is excellent for learning and easy backups. If the app grows, moving to SQLite or PostgreSQL is a natural next step.
This project is not just "I made a Flask app." It shows the full path from code to server.
These are the links I would keep open while building the app.
At the end, I have a real personal web app running on a cheap cloud server. LaunchShell.com hosts my Seed App, and it has a public domain, HTTPS, a Linux service, a reverse proxy, a backup plan, and data I actually care about. That is a much stronger project than another throwaway tutorial.