Prerequisites
This guide walks you through deploying a Python Flask application to AWS Elastic Beanstalk — from a working local app to a live, publicly accessible URL. We'll configure the EB environment, handle dependencies correctly, and avoid the configuration traps that silently break deployments.
Before starting, make sure you have: an AWS account with IAM permissions for Elastic Beanstalk and S3, Python 3.12 installed locally, the AWS CLI configured (aws configure), and pip available. Basic Flask knowledge is assumed.
Setup: Project Structure and Dependencies
Elastic Beanstalk expects a specific project layout. The most important constraint: your entry point file must be named application.py, and the Flask app object inside it must be named application. This is not optional — EB's Python platform looks for exactly these names.
Start by creating a fresh project directory and a virtual environment:
mkdir flask-eb-demo
cd flask-eb-demo
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install flask==3.0.3 gunicorn==22.0.0
pip freeze > requirements.txt
We're installing Gunicorn alongside Flask because EB uses it as the WSGI server in production. The built-in Flask development server is not suitable for production and EB will not use it.
Your project structure should look like this when we're done:
flask-eb-demo/
├── application.py
├── requirements.txt
├── .ebextensions/
│ └── python.config
├── .ebignore
└── templates/
└── index.html
Building the Flask Application
Create the main application file. The variable name application (not app) is what makes EB's auto-discovery work:
from flask import Flask, render_template, jsonify
import os
import datetime
application = Flask(__name__)
@application.route("/")
def index():
return render_template("index.html", timestamp=datetime.datetime.utcnow())
@application.route("/health")
def health():
return jsonify({"status": "ok", "time": datetime.datetime.utcnow().isoformat()})
@application.route("/api/greet/")
def greet(name: str):
if not name.isalpha():
return jsonify({"error": "Name must contain only letters"}), 400
return jsonify({"message": f"Hello, {name}!", "served_by": os.environ.get("HOSTNAME", "unknown")})
if __name__ == "__main__":
application.run(debug=False, port=5000)
The /health endpoint is practical, not decorative. Elastic Beanstalk's load balancer pings a health check URL to decide whether your instances are alive. We'll point it at this route shortly.
Now create the template directory and a minimal HTML page:
mkdir templates
# templates/index.html — save this as an HTML file, not Python
cat > templates/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flask on Elastic Beanstalk</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 4rem auto; padding: 0 1rem; }
.badge { background: #232f3e; color: #ff9900; padding: .3rem .8rem; border-radius: 4px; font-size: .85rem; }
</style>
</head>
<body>
<h1>Flask App <span class="badge">AWS EB</span></h1>
<p>Deployed and running. Server time: {{ timestamp.strftime('%Y-%m-%d %H:%M:%S') }} UTC</p>
<p>Try <a href="/api/greet/World">/api/greet/World</a> or <a href="/health">/health</a></p>
</body>
</html>
EOF
Test that everything runs locally before touching AWS:
python application.py
Visit http://localhost:5000. You should see the HTML page with the current UTC timestamp. If you see a 500 error, check that your templates/ folder is in the same directory as application.py.
Configuring Elastic Beanstalk
Create the EB configuration directory and a config file that tells the platform which WSGI module to load and sets environment-level options:
mkdir .ebextensions
# .ebextensions/python.config
option_settings:
aws:elasticbeanstalk:container:python:
WSGIPath: application:application
aws:elasticbeanstalk:environment:proxy:staticfiles:
/static: static
aws:elasticbeanstalk:application:environment:
FLASK_ENV: production
PYTHONUNBUFFERED: "1"
The WSGIPath value application:application means "the application object inside the application.py module." The first part is the filename (without .py), the second is the variable name.
⚠️ Note: If you named your fileapp.pyor your Flask objectapp, the deployment will appear to succeed but your app will return 502 errors. Rename both toapplicationbefore deploying.
Next, create a .ebignore file so the EB CLI doesn't upload your virtual environment and cache files (this dramatically reduces upload size and deploy time):
cat > .ebignore << 'EOF'
.venv/
__pycache__/
*.pyc
*.pyo
.git/
.DS_Store
*.egg-info/
dist/
build/
EOF
Now install the EB CLI and initialize the project. The EB CLI is separate from the AWS CLI:
pip install awsebcli==3.21.0
eb init flask-eb-demo --platform "Python 3.12" --region us-east-1
The eb init command creates a hidden .elasticbeanstalk/ directory with your app configuration. When prompted about CodeCommit, enter n — we don't need it for this deployment.
Create the environment and deploy in one step:
eb create flask-production \
--instance-type t3.micro \
--single \
--envvars FLASK_ENV=production
The --single flag creates a single-instance environment without a load balancer, which keeps costs near zero for demo purposes. For a production workload, remove this flag and EB will provision a load-balanced, auto-scaling environment.
⚠️ Note: The firsteb createtakes 5–10 minutes. It's provisioning an EC2 instance, security groups, an S3 bucket for your application versions, and CloudWatch log groups. Don't interrupt it with Ctrl+C — if it fails partway through, runeb terminate flask-productionand start fresh rather than debugging a half-built environment.
Testing and Verification
Once the environment shows Environment successfully launched, open the app directly from the terminal:
eb open
This opens your default browser to the EB-generated URL (something like flask-production.us-east-1.elasticbeanstalk.com). You should see the same HTML page you tested locally, now served from AWS.
Verify the API endpoints work correctly:
# Replace the URL with your actual EB environment URL
EB_URL=$(eb status | grep "CNAME" | awk '{print $2}')
curl "http://$EB_URL/health"
# Expected: {"status": "ok", "time": "2024-11-15T14:22:31.004512"}
curl "http://$EB_URL/api/greet/Alice"
# Expected: {"message": "Hello, Alice!", "served_by": "ip-172-31-xx-xx"}
curl "http://$EB_URL/api/greet/Alice123"
# Expected: {"error": "Name must contain only letters"} — with HTTP 400
Check the live application logs if something isn't responding correctly:
eb logs --all
The most useful log for Python errors is /var/log/web.stdout.log. If you see ModuleNotFoundError, your requirements.txt is either missing a package or wasn't generated from the active virtual environment. Re-run pip freeze > requirements.txt with your .venv activated, then redeploy with eb deploy.
⚠️ Note: After any code change, redeploy witheb deploy(noteb create). Thedeploycommand zips your project, uploads it to S3, and performs a rolling update. Runningeb createagain tries to provision a second environment.
To confirm the health check endpoint is wired up properly, check the environment health dashboard:
eb health
You should see your instance listed as Ok with a green status. If it shows Degraded, EB is likely hitting the default / health check path and getting an unexpected status code — verify your root route returns HTTP 200.
What We Built and Where to Go Next
You now have a Python Flask application running on AWS Elastic Beanstalk with a proper WSGI configuration, environment-specific settings, and working health checks. The deployment pipeline is repeatable: make changes locally, test with python application.py, then push with eb deploy.
From here, the natural extensions are: adding a custom domain via Route 53 and attaching an SSL certificate through ACM (run eb config to set the load balancer listener), connecting a PostgreSQL database by provisioning an RDS instance inside the EB environment, and setting up environment variables for secrets using eb setenv SECRET_KEY=your-value rather than hardcoding them. When you're done experimenting, run eb terminate flask-production to avoid ongoing charges.