Oct 26, 20267 min read

How to Deploy a Flask App to AWS

Running Flask with Postgres and S3 on AWS with Encore, Elastic Beanstalk, ECS on Fargate or Zappa on Lambda, with a production WSGI server, a non-root container and the networking each one needs.

Flask runs anywhere Python does, and on AWS that means choosing between Beanstalk, ECS, Lambda and EC2 before adding RDS, S3, IAM roles and a WSGI server. A small app with a database and file uploads touches most of them before its first production request.

This guide deploys a books API with Postgres and S3 cover uploads to AWS. It compares Encore, Elastic Beanstalk, ECS on Fargate and Zappa.

What you need to decide:

  • Containers or functions.
  • How RDS and S3 are created and who can reach them.
  • Which WSGI server runs the app and with how many workers.
  • How local development matches production.

Flask on AWS options at a glance

CapabilityEncoreElastic BeanstalkECS on FargateZappa on Lambda
ComputeFargate in your accountEC2 instances behind a load balancerFargate tasks behind a load balancerLambda behind API Gateway
PostgresRDS from the declarationCreate RDS yourselfCreate RDS yourselfCreate RDS and usually RDS Proxy
S3 uploadsBucket from the declaration, an IAM role for the serviceCreate bucket and instance roleCreate bucket and task roleCreate bucket and function role
Local developmentencore run starts Postgres and a local bucketYou run themYou run themYou run them
Infrastructure definitionYour Flask codeConsole, EB CLI or .ebextensionsTerraform, CDK or consolezappa_settings.json plus manual resources

Encore: deploy Flask to AWS from your code

Encore

Encore combines the database and bucket your Flask app declares with a local environment and a deployment to your own AWS account, so RDS, S3 and the Fargate service all come from the code you run locally, and Encore provisions an IAM role for the service. It is an infrastructure SDK you add to an existing app, and the views stay plain Flask.

Encore declares infrastructure in Python code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in Python, running locally under encore run and provisioned in your own AWS or GCP account on deploy.

The books table is a SQL migration in ./migrations:

-- migrations/1_create_books.up.sql CREATE TABLE books ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL );

The app declares a Postgres database and a covers bucket at module level. Uploads go to the bucket with upload, and downloads use a presigned URL that expires after five minutes, so the bucket never has to be public:

import os from encore.storage import objects, postgres from flask import Flask, abort, request from waitress import serve # On AWS: an RDS Postgres database and an S3 bucket in your account. # Under `encore run`: a local Postgres database and a local bucket. db = postgres.Database("books", migrations="./migrations") covers = objects.Bucket("covers") app = Flask(__name__) @app.post("/books") async def create_book(): data = request.get_json() row = await db.query( "INSERT INTO books (title, author) VALUES ($1, $2) RETURNING id", data["title"], data["author"], ).one() return {"id": row["id"]}, 201 @app.get("/books/<int:book_id>") async def get_book(book_id: int): row = await db.query( "SELECT id, title, author FROM books WHERE id = $1", book_id ).first() if row is None: abort(404) return {"id": row["id"], "title": row["title"], "author": row["author"]} @app.put("/books/<int:book_id>/cover") async def upload_cover(book_id: int): attrs = await covers.upload( f"{book_id}.jpg", request.get_data(), content_type="image/jpeg" ) return {"key": f"{book_id}.jpg", "size": attrs.size} @app.get("/books/<int:book_id>/cover") async def cover_url(book_id: int): if not await covers.exists(f"{book_id}.jpg"): abort(404) url = await covers.signed_download_url(f"{book_id}.jpg", ttl=300) return {"url": url} @app.get("/healthz") def healthz(): return {"ok": True} if __name__ == "__main__": # Encore's local run sets ENCORE_LISTEN_ADDR to the "host:port" to bind. addr = os.environ.get("ENCORE_LISTEN_ADDR") if addr: host, _, port = addr.rpartition(":") else: host, port = "0.0.0.0", os.environ.get("PORT", "8000") serve(app, host=host or "0.0.0.0", port=int(port))

encore run starts Postgres locally with the migration applied and a local bucket that issues working signed URLs, so the upload flow can be tested end to end before anything exists on AWS:

~/books
$ encore run$ curl -X PUT localhost:4000/books/1/cover --data-binary @cover.jpg{"key":"1.jpg","size":15}$ curl localhost:4000/books/1/cover{"url":"http://127.0.0.1:40417/objects/covers/1.jpg?X-Goog-Algorithm=..."}

On deploy, Encore provisions an RDS Postgres instance in private subnets, an S3 bucket and a Fargate service with a load balancer in your AWS account, and provisions an IAM role for the service. The app needs no long-lived keys, since permissions are granted through a role in your account, and cloud access sits with Encore's IAM role, not the agent. Preview environments for pull requests run in Encore's hosting with their own database and bucket, while staging and production live in your account.

Good to know

The upload route accepts any body, so add authentication and a size limit, for example Flask's MAX_CONTENT_LENGTH, before exposing it. Waitress is used here because it runs on every platform, and Gunicorn works just as well.

Try Encore →

Elastic Beanstalk

Elastic Beanstalk's Python platform looks for a WSGI callable named application in application.py, and the Flask walkthrough deploys it with the EB CLI:

~/books
$ eb init -p python-3.13 books --region eu-central-1$ eb create books-prod --instance-types t3.small$ eb setenv DATABASE_URL=postgresql+psycopg://... COVERS_BUCKET=books-covers-prod$ eb deploy

Good to know

Creating RDS from inside Beanstalk ties the database's lifetime to the environment, so most teams create it separately and pass the URL as an environment property. The instance profile needs an S3 policy for the bucket, and Beanstalk runs Gunicorn for you, configurable through a Procfile.

ECS on Fargate with a Dockerfile

For containers, the app gets a Dockerfile that runs Gunicorn as a non-root user, and the image is pushed to ECR and run as a Fargate service:

FROM python:3.13-slim ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt gunicorn COPY . . RUN useradd --create-home --uid 10001 app USER app EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-", "main:app"]

Good to know

Around the task definition you need a VPC with private subnets, an Application Load Balancer, a target group with a health check on /healthz, a task role for S3, a task execution role for pulling the image and reading secrets, and RDS with a security group that admits the tasks. That is usually written in Terraform or the CDK. The Gunicorn documentation suggests starting at (2 x cores) + 1 workers, which on a 1 vCPU task is three. ECS Express Mode, which AWS launched in 2025 as the successor to App Runner's simple setup, removes some of this for basic web services.

Zappa on Lambda

Zappa packages a WSGI app for Lambda behind API Gateway, configured in zappa_settings.json:

{ "production": { "app_function": "main.app", "aws_region": "eu-central-1", "runtime": "python3.13", "s3_bucket": "books-zappa-deployments", "environment_variables": { "COVERS_BUCKET": "books-covers-prod" } } }

Good to know

Ordinary Flask views work unchanged, but Lambda limits request payloads to a few megabytes, so large covers should go straight to S3 with presigned upload URLs. Each concurrent request opens its own database connection, and RDS Proxy is the usual fix. Check that the Zappa release you install supports the Python runtime you plan to use.

Running Flask well on AWS

The Flask documentation's deployment page is clear that the development server is not for production, and on AWS the WSGI server sits behind a load balancer, so enable ProxyFix from Werkzeug to read the client's real IP and scheme. Keep uploads in S3 rather than on the container's disk, which does not survive a deploy. With Encore, the RDS instance, S3 bucket, roles and network are created in your account from the code.

How to choose

  • Encore if you want RDS, S3, Fargate and an IAM role for the service created in your AWS account from the Flask code, with the same setup locally.
  • Elastic Beanstalk for a single app where clicking through the defaults is acceptable.
  • ECS on Fargate if your team already writes Terraform or CDK and wants full control.
  • Zappa on Lambda for low, spiky traffic and small payloads.

Python support in Encore is in beta, and you can sign up for access to try it with your Flask app.

Frequently asked questions

What is the easiest way to deploy Flask to AWS?

Elastic Beanstalk takes a zip with application.py and requirements.txt and runs it behind a load balancer, which is quick for a single app. With Encore, declare postgres.Database("books", migrations="./migrations") and objects.Bucket("covers") in the Flask code and connect your AWS account, and Encore creates the Fargate service, RDS database and S3 bucket in that account on each push.

Should Flask run on Lambda or containers on AWS?

Containers on Fargate suit steady traffic, long requests and apps with connection pools. Lambda through Zappa or a WSGI adapter suits spiky, low traffic, but each concurrent request runs in its own environment and opens its own database connection, which usually calls for RDS Proxy.

Do I need Gunicorn to run Flask in production?

Flask's built-in server is for development only. Use a production WSGI server such as Gunicorn or Waitress, and size the worker count to the CPU and memory of the container.

How do I store user uploads from Flask on AWS?

Write them to S3 rather than the container's disk, which is lost on every deploy. Serve them with short-lived presigned URLs so the bucket can stay private.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders