Logan Nommensen
Deploying Flask Apps on AWS

Deploying Flask Apps on AWS

A beginner's guide following my experience deploying a Flask app, including a database, on AWS.

September 16, 202513 min read

I recently decided to deploy my SimpleChat app publicly and robustly (previously being ran manually on my VPS). I had limited experience with cloud computing (mainly Google Cloud Platform), so I decided to learn more about AWS due to its large market share, and to compare.

The App Itself

SimpleChat is a real-time chatroom application built with Flask (Python) and Socket.IO. It uses a database for storing messages and user information, which can be either SQLite (for local development) or PostgreSQL (for production). The app is containerized with Docker, making for easy deployment. Exact code is not the focus of this post, but the source code is available on GitHub.

Containerization

Containerization is the process of packaging an application into a container, which can be run on any system that supports the container software (e.g. Docker). This allows for easy deployment and consistency across different environments, similar to a virtual machine, but much more lightweight as it shares the host's system resources directly.

This tutorial uses Docker, which is the most popular container software. You can find installation instructions for your OS on Docker's website, or this handy guide for installing on Linux servers.

Once Docker is installed, you want to create a Dockerfile in the root of your project. This file contains the instructions for building the container image. SimpleChat's `Dockerfile is below:

# Specify the base image, in this case an image with Python 3.13 installed
FROM python:3.13

# Set the working directory inside the container, host OS is unaffected
WORKDIR /app

# Copy requirements (in root of project) and install them, including Gunicorn for running in production
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install gunicorn

# Copy the rest of the application code into the container and make the boot script executable
COPY . .
RUN chmod a+x boot.sh

# Set environment variable for Flask app
ENV FLASK_APP=simplechat:create_app

# Expose port 5000 for the app to be accessible outside the container
EXPOSE 5000

# Specify the command (in this case, script) to run when the container starts
CMD ["./boot.sh"]

A few things to note:

  • Requirements are installed first, as they are less likely to change than the application code. This allows for Docker's caching to speed up subsequent rebuilds if only the application code changes.
  • As the app will be going into production, Flask is run with Gunicorn, as opposed to the built-in Flask development server. Gunicorn is production-ready and can handle multiple requests concurrently, running much more efficiently and securely than the development server.
  • The boot.sh script is used to run the app with Gunicorn, as it allows for easily changing the command without modifying the Dockerfile. The script is a simple, updating any database schema (if using flask-sqlalchemy) and starting Gunicorn with the appropriate options:
#!/bin/bash
flask db upgrade
exec gunicorn -b :5000 --access-logfile - --error-logfile - --worker-class eventlet -w 1 run:app

With the Dockerfile and boot.sh script created, you can build the container image with the following command (in the root of your project, where the Dockerfile is located):

docker build -t simplechat:latest .

This command builds the image and tags it with the name simplechat and the tag latest. You can verify the image was built successfully with docker images, which lists all locally available images.

docker images command output showing simplechat image

Running Locally

To ensure your image works, you can run the container locally with the following command:

docker run --name simplechat -p 5000:5000 -d --rm simplechat:latest

This command runs the container in detached mode (-d), mapping port 5000 on the host to port 5000 in the container (-p 5000:5000), and removes the container (not the image) when it stops (--rm).

Amazon Web Services (AWS)

Here is where things get more complex in my opinion. AWS is a massive platform with many services, and it can be overwhelming to navigate. For this tutorial, we will be using the following services:

  • ECR (Elastic Container Registry): A managed Docker container registry for storing and managing Docker container images.
  • ECS (Elastic Container Service): A container orchestration service that makes it easy to deploy, manage, and scale containerized applications.
  • RDS (Relational Database Service): A relational database service (who knew) that hosts and manages databases such as PostgreSQL.
  • IAM (Identity and Access Management): A service for managing access to AWS resources securely.
  • AWS Certificate Manager (ACM): A service for provisioning, managing, and deploying SSL/TLS certificates for use within AWS services.
  • Secrets Manager: A service for securely storing and managing sensitive information such as database credentials.

Creating an RDS Instance

To create a PostgreSQL database for your app, navigate to the RDS service in the AWS Management Console and click "Create database". Select "Easy Create", then select "PostgreSQL" as the engine type. Settings can largely be left as default, however you will need to select "Publicly accessible" under Connectivity > Additional configuration. The rest can be changed as desired, such as instance name, username, and password. Do make note of the username and password, however. After a few minutes, your database should be created and available. Try connecting to it using a database client to verify.

You will want to modify your app's configuration to use the RDS database, likely through environment variables. For SimpleChat, I check for an environment variable DATABASE_URI, defaulting to a local SQLite database if not found.

Creating Secrets! Shhh!

Don't add secrets such as database credentials, Flask SECRET_KEY, etc. to your code, Dockerfile, or Docker image (since all files in your directory will be added to the container).

Instead, use AWS Secrets Manager to store your secrets as plaintext. In the later ECS Task Defining, you will be able to use these secrets as environment variables in your container.

Navigate to Secrets Manager in the AWS Management Console and click "Store a new secret". Select "Other type of secret", then add either the key-value pairs for your secrets, or just plaintext for one secret. Copy the created secrets' ARNs, since these will be used next, and in the ECS task definition step.

Allowing ECS Access to Secrets

To allow your ECS tasks to access the secrets, you will need to create a new IAM role. Navigate to the IAM service in the AWS Management Console and click "Roles" in the sidebar, finding the ecsTaskExecutionRole role, and add a new permission with an inline policy. Use JSON and copy and paste the following policy, modifying the "Resource" field to match your secrets' ARNs:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "(ARN_1)",
                "(ARN_2)"
            ]
        }
    ]
}

Creating an ECR Repository

Setting Up AWS CLI

To push your Docker container images, you need to set up the AWS CLI (Command Line Interface) on your local machine. This allows you to interact with AWS services from the command line. You can find installation instructions on AWS's website.

Once installed, you need to configure the CLI with your AWS credentials, as found on the setup instructions.

Creating the Repository

One the CLI is set up, you can create an ECR repository on the AWS Management Console by searching for "ECR." You can then click "Create repository" and give it a name (e.g. chat) in a namespace (e.g. nommensen) for your repository.

Pushing your Images

AWS provides the copyable commands to push your image to ECR inside the repository page. Click on "View push commands" to see the commands. You will need to run these commands in the terminal you installed AWS CLI in, in the directory of your project (if not already built). Once pushed, refresh your repository to see the new image.

Creating an ECS Cluster and Task Definition

Once your image is pushed to ECR, you can navigate to the ECS service. Create a new cluster, making sure to select "AWS Fargate" as the cluster template. You can leave most settings as default, however you may want to change the cluster name and Monitoring > Container Insights > Observability to enhanced.

Once created, you must define what will run on your cluster, in what is called a Task Definition. Click on Task definitions on the sidebar, and create a new Task Definition. Again, select "Fargate" as the launch type. Specify the OS/resources necessary for your app (SimpleChat is so simple, I selected just 0.25 vCPU and 0.5 GB memory). You will need to select a task role.

Next, you will need to add a container to the task definition. Give it a name (e.g. chat) and specify the image URI from ECR (found on the repository page). You will need to specify the memory and CPU units again, this time for the container specifically (as opposed to the whole task). You will also need to specify the port mapping, which for a Flask app is likely 5000 (unless you changed it, which isn't usually necessary).

In the Environment variables section, add the keys for the environment variables your app needs, such as SECRET_KEY, and if they are stored in Secrets Manager, select "ValueFrom" and paste the secret's ARN. You can also add non-secret environment variables here as well.

Deploying the Container

To finally create the running process, you need to create a Service from the Task Definition. This will create a long-running process based on your Task Definition.

Most settings can be left as default, however you will want to enable a Public IP in the Networking section, and enable load balancing.

Using load balancing will allow Amazon Elastic Load Balancer (ELB) to distribute the incoming traffic to your Flask app's port 5000, and run continual health checks to ensure the app is running. Here, you want to change the target group port to 5000 to match the port your app is running on.

Load balancing settings

Once you create the service, you may need to wait a few minutes for all the resources to be created and started. Once the service shows up as active (hopefully!) in Clusters view open the Service and click on the Load Balancer link. In the Security tab, click the security group and edit inbound rules to add a new rule allowing HTTP traffic (port 80) from any source.

Security group inbound rules

Now you can finally access your Flask app by going to the Load Balancer's DNS name (found on the load balancer's page)!

SimpleChat app running on AWS

Custom Domain and HTTPS

If you have your own domain (you should, they're quite cheap and easy to mess with), you can set up a CNAME record to point to your load balancer's DNS name. This will allow you to access your app using your own (sub)domain.

CNAME record for chat.nommensen.dev

You may notice you are unable to reach your app using your custom domain in your browser, but may be able to via curl (.dev domains are HTTPS-only), or get a security warning. This is because you need to set up HTTPS for your domain. You can do this using AWS Certificate Manager (ACM) to create a free SSL/TLS certificate for your domain.

Navigate to AWS Certificate Manager and request a public certificate for your domain (e.g. chat.nommensen.dev), then complete the verification process (I used DNS verification, which involves adding a CNAME record to your domain's DNS settings as shown below).

ACM Domain verification step showing cname name and target to be added to DNS Cloudflare DNS settings showing cname record for domain verification

Once your CNAME record is added, you may need to wait a few minutes for the DNS to propagate and for AWS to verify. Once verified, head back to your load balancer's page and edit the listener to add a new rule for HTTPS (port 443). You will need to select the certificate you just created in ACM.

Load balancer listener settings showing https listener with certificate

Now you should be able to access your app using your custom domain with HTTPS!

CI/CD with GitHub Actions

If you would like to automate the deployment process on every new push to main (or when a new tag is created), you can use GitHub Actions to create a CI/CD pipeline to push the new image to ECR and update the ECS service.

GitHub has a (somewhat) handy guide on how to do this, however you need to do/know a few things first:

  • You will need to create a new IAM user with programmatic access and appropriate permissions. Don't ask me the permissions, I couldn't figure it out and just gave it admin access 🫣
  • You will need to add the IAM user's access key ID and secret access key as GitHub secrets in your GH repo.
  • You will need to know your AWS region, ECR repository name, and ECS cluster and service names.
  • You will need your task definition JSON file, specifically the creation definition. You can get this by going to your task definition and selecting "Create new revision", then copying the JSON from the "JSON" tab. For some reason (likely) the old version of the Action in the tutorial), you have to remove the enableFaultInjection entry.

Once you have all that, you can create a new GitHub Actions workflow in your repo (e.g. .github/workflows/deploy.yml) and place the task definition JSON file in a .aws folder in your repo (e.g. .aws/task-definition.json)

This may take a few tries to get right, but you can check the SimpleChat repo for reference. I also found the Amazon Q bot quite useful for troubleshooting if the issue is with the AWS configuration.

Conclusion

Deploying on AWS can be a very daunting experience, especially for beginners. However, with some patience you should be able to get your Flask app up and running! This process took me a few days of trial and error, including writing this post, but I learned a lot about AWS and cloud computing, and gained an appreciation (the hard way) for the:

  • Reliability: My app kept running a working build despite many errors and crashes
  • Security: The granularity of permissions and roles, while complex and daunting, can be very secure and allows for great control to follow the principle of least privilege.
  • Customization: The ability to add logging, monitoring, scaling, load balancing, etc. along every step of the way shows the power of a mature cloud platform with tons of services and features.

If you have any questions or comments, feel free to reach out to me on the links in the footer, I'd be happy to chat!

Happy deploying (and good luck)!