Yandex Cloud
Search
Contact UsGet started
  • Blog
  • Pricing
  • Documentation
  • All Services
  • System Status
    • Featured
    • Infrastructure & Network
    • Data Platform
    • Containers
    • Developer tools
    • Serverless
    • Security
    • Monitoring & Resources
    • ML & AI
    • Business tools
  • All Solutions
    • By industry
    • By use case
    • Economics and Pricing
    • Security
    • Technical Support
    • Customer Stories
    • Start testing with double trial credits
    • Cloud credits to scale your IT product
    • Gateway to Russia
    • Cloud for Startups
    • Education and Science
    • Yandex Cloud Partner program
  • Blog
  • Pricing
  • Documentation
© 2025 Direct Cursus Technology L.L.C.
Tutorials
    • All tutorials
      • Transferring a WordPress website from a different hosting provider to Yandex Cloud
      • Setting up virtual hosting
      • Creating a Python web application with Flask
      • Hosting a static Gatsby website
      • Migrating to Cloud CDN from a third-party CDN provider
      • Getting website traffic statistics with S3 Select

In this article:

  • Getting started
  • Create a cloud network and subnet
  • Create a security group
  • Create and set up a virtual machine
  • Create and run the application
  • Create and configure the HTML templates
  • Create the main page HTML template
  • Configure other HTML templates
  • Configure the database
  • Display blog posts
  • Display the blog posts list
  • Display individual blog posts
  • Add post actions
  • Creating a new post
  • Editing an existing post
  • Deleting a post
  • Summarize the results
  • Delete the resources you created
  1. Application solutions
  2. Creating a website
  3. Creating a Python web application with Flask

Creating a Python web application with Flask

Written by
Yandex Cloud
Updated at May 7, 2025
  • Getting started
    • Create a cloud network and subnet
    • Create a security group
  • Create and set up a virtual machine
  • Create and run the application
  • Create and configure the HTML templates
    • Create the main page HTML template
    • Configure other HTML templates
  • Configure the database
  • Display blog posts
    • Display the blog posts list
    • Display individual blog posts
  • Add post actions
    • Creating a new post
    • Editing an existing post
    • Deleting a post
  • Summarize the results
  • Delete the resources you created

Using this tutorial, you will develop a simple web application from scratch: a mini blog where users can create, view, edit, and delete posts. To do this, you will create a Yandex Compute Cloud VM in Yandex Cloud and run a web server on it.

The web app is written in Python and uses SQLite to handle data. To develop the frontend, we will use Bootstrap.

We opted for Flask as the base framework for our application, since it is one of the most intuitive and straightforward Python web frameworks.

Flask allows you to create web applications from a single source code file; it does not require following any specific directory structure or writing complex template code prior to using it. Flask also provides out-of-the-box support for the Werkzeug toolkit and the Jinja templating engine to create dynamic HTML pages.

You can find the source code for the application in the flask_blog.zip file.

How your application is assumed to work:

  1. A Linux Ubuntu VM with a Flask web server installed and running gets HTTP requests from the user’s browser.
  2. The web server forwards a request to the web application, whose router invokes the appropriate handler function based on the request URL.
  3. The handler function makes a query to the SQLite database to retrieve or write the required data.
  4. The function sends the data retrieved from the database to the appropriate Jinja HTML template, which returns the final HTML code of the page.
  5. This HTML code is forwarded to the web server, which, in turn, transfers this code to the user's browser.

To create a web application with Flask:

  1. Get your cloud ready.
  2. Create and set up a virtual machine.
  3. Create and run the application.
  4. Create and configure the HTML templates.
  5. Configure the database.
  6. Configure the display of blog posts.
  7. Add actions with posts.
  8. Summarize the results.

If you no longer need the web application, delete the resources it uses.

Getting started

In Yandex Cloud, you only pay for the resources you consume and the time of their actual use. To get identified as a payer, you need a billing account.

The cost of support for your web application includes:

  • Fee for using a public IP address (see Yandex Virtual Private Cloud pricing).
  • Fee for VM computing resources and disks (see Yandex Compute Cloud pricing).

Sign up in Yandex Cloud and create a billing account:

  1. Navigate to the management console and log in to Yandex Cloud or register a new account.
  2. On the Yandex Cloud Billing page, make sure you have a billing account linked and it has the ACTIVE or TRIAL_ACTIVE status. If you do not have a billing account, create one and link a cloud to it.

If you have an active billing account, you can navigate to the cloud page to create or select a folder for your infrastructure to operate in.

Learn more about clouds and folders.

Create a cloud network and subnet

In Yandex Cloud, resources are linked one to another and to the internet with cloud networks, where resources receive public IP addresses as well as private IP address ranges or subnets.

To create a virtual network and subnet for your web server:

Management console
CLI
API
  1. In the management console, select your folder.
  2. In the list of services, select Virtual Private Cloud.
  3. At the top right, click Create network.
  4. In the Name field, specify webserver-network.
  5. In the Advanced field, disable the Create subnets option.
  6. Click Create network.
  7. In the left-hand panel, select Subnets.
  8. At the top right, click Create.
  9. In the Name field, specify webserver-subnet-ru-central1-b.
  10. In the Zone field, select the ru-central1-b availability zone.
  11. In the Network field, select webserver-network.
  12. In the CIDR field, specify 192.168.1.0/24.
  13. Click Create subnet.
  1. Create a network named webserver-network:

    yc vpc network create webserver-network
    

    Result:

    id: enp1gg8kr3pv********
    folder_id: b1gt6g8ht345********
    created_at: "2023-12-20T20:08:11Z"
    name: webserver-network
    default_security_group_id: enppne4l2eg5********
    

    For more information about the yc vpc network create command, see the CLI reference.

  2. Create a subnet in the ru-central1-b availability zone:

    yc vpc subnet create webserver-subnet-ru-central1-b \
      --zone ru-central1-b \
      --network-name webserver-network \
      --range 192.168.1.0/24
    

    Result:

    id: e2li9tcgi7ii********
    folder_id: b1gt6g8ht345********
    created_at: "2023-12-20T20:11:16Z"
    name: webserver-subnet-ru-central1-b
    network_id: enp1gg8kr3pv********
    zone_id: ru-central1-b
    v4_cidr_blocks:
      - 192.168.1.0/24
    

    For more information about the yc vpc subnet create command, see the CLI reference.

  1. To create a network, use the create REST API method for the Network resource or the NetworkService/Create gRPC API call.

  2. To create a subnet, use the create REST API method for the Subnet resource or the SubnetService/Create gRPC API call.

Create a security group

Security groups are the central tool for managing network access in Yandex Cloud and help restrict unauthorized traffic to cloud resources at the cloud network level.

Create a security group that allows inbound TCP traffic on ports 5000 and 22 as well as any outbound traffic. Port 22 is used to connect to the VM over SSH, while 5000 is the default port to run a Flask web server.

Management console
CLI
API
  1. In the management console, select your folder.

  2. In the list of services, select Virtual Private Cloud.

  3. In the left-hand panel, select Security groups.

  4. Click Create security group.

  5. In the Name field, enter this name: webserver-sg.

  6. In the Network field, select webserver-network, which you created earlier.

  7. Under Rules, create the following traffic management rules:

    Traffic
    direction
    Description Port range Protocol Source /
    Destination name
    CIDR blocks
    Inbound Flask 5000 TCP CIDR 0.0.0.0/0
    Inbound ssh 22 TCP CIDR 0.0.0.0/0
    Outbound any All Any CIDR 0.0.0.0/0
  8. Click Save.

Run this command:

yc vpc security-group create \
  --name webserver-sg \
  --rule "description=Flask,direction=ingress,port=5000,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
  --rule "description=ssh,direction=ingress,port=22,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
  --rule "description=any,direction=egress,port=any,protocol=any,v4-cidrs=[0.0.0.0/0]" \
  --network-name webserver-network

Result:

id: enpv1c0q7j01********
folder_id: b1gt6g8ht345********
created_at: "2024-03-23T18:54:05Z"
name: webserver-sg
network_id: enp9mji1m7b3********
status: ACTIVE
rules:
  - id: enpmbsk2hjfd********
    description: Flask
    direction: INGRESS
    ports:
      from_port: "5000"
      to_port: "5000"
    protocol_name: TCP
    protocol_number: "6"
    cidr_blocks:
      v4_cidr_blocks:
        - 0.0.0.0/0
  - id: enpna5id9265********
    description: ssh
    direction: INGRESS
    ports:
      from_port: "22"
      to_port: "22"
    protocol_name: TCP
    protocol_number: "6"
    cidr_blocks:
      v4_cidr_blocks:
        - 0.0.0.0/0
  - id: enpen3vf7rui********
    description: any
    direction: EGRESS
    protocol_name: ANY
    protocol_number: "-1"
    cidr_blocks:
      v4_cidr_blocks:
        - 0.0.0.0/0

Save the id as you will need need it to create a VM.

For more information about the yc vpc security-group create command, see the CLI reference.

To create a security group, use the create REST API method for the SecurityGroup resource or the SecurityGroupService/Create gRPC API call.

Create and set up a virtual machine

A virtual machine is similar to a server in cloud infrastructures. In Yandex Cloud, you can create VMs with varying hardware specifications in terms of performance, RAM, and disk space; they can also run on various operating systems.

This web application will be deployed on an Ubuntu 22.04 LTS VM.

  1. Create a virtual machine:

    Before you start, prepare a key pair (a public key and a private key) to access your VM over SSH.

    Management console
    CLI
    API
    1. In the management console, select the folder where you want to create your VM.

    2. In the list of services, select Compute Cloud.

    3. In the left-hand panel, select Virtual machines.

    4. Click Create virtual machine.

    5. Under Boot disk image, select the Ubuntu 22.04 LTS image.

    6. Under Location, select the ru-central1-b availability zone.

    7. Under Network settings:

      • In the Subnet field, select the webserver-subnet-ru-central1-b subnet you created earlier.
      • In the Public IP address field, select Auto.
      • In the Security groups field, select webserver-sg, which you created earlier.
    8. Under Access, select SSH key and specify the VM access credentials:

      • Enter the username in the Login field: yc-user.
      • In the SSH key field, select the SSH key saved in your organization user profile.

        If there are no saved SSH keys in your profile, or you want to add a new key:

        • Click Add key.
        • Enter a name for the SSH key.
        • Upload or paste the contents of the public key file. You need to create a key pair for the SSH connection to a VM yourself.
        • Click Add.

        The SSH key will be added to your organization user profile.

        If users cannot add SSH keys to their profiles in the organization, the added public SSH key will only be saved to the user profile of the VM being created.

    9. Under General information, specify the VM name: sftp-server.

    10. Click Create VM.

    Run the command while specifying the security group ID you saved at the previous step:

    yc compute instance create \
      --name mywebserver \
      --zone ru-central1-b \
      --network-interface subnet-name=webserver-subnet-ru-central1-b,nat-ip-version=ipv4,security-group-ids=<security_group_ID> \
      --create-boot-disk image-folder-id=standard-images,image-id=fd8ne6e3etbrr2ve9nlc \
      --ssh-key <public_SSH_key_file>
    

    Where --ssh-key is the path to the public SSH key file. It can look like this: ~/.ssh/id_ed25519.pub.

    Result:

    done (32s)
    id: epdv79cu67np********
    folder_id: b1gt6g8ht345********
    created_at: "2024-03-23T19:17:09Z"
    name: mywebserver
    zone_id: ru-central1-b
    platform_id: standard-v2
    resources:
      memory: "2147483648"
      cores: "2"
      core_fraction: "100"
    status: RUNNING
    metadata_options:
      gce_http_endpoint: ENABLED
      aws_v1_http_endpoint: ENABLED
      gce_http_token: ENABLED
      aws_v1_http_token: DISABLED
    boot_disk:
      mode: READ_WRITE
      device_name: epdg0926k12t********
      auto_delete: true
      disk_id: epdg0926k12t********
    network_interfaces:
      - index: "0"
        mac_address: d0:0d:1f:3a:59:e3
        subnet_id: e2l3qffk0h6t********
        primary_v4_address:
          address: 192.168.1.14
          one_to_one_nat:
            address: 62.84.***.***
            ip_version: IPV4
        security_group_ids:
          - enpv1c0q7j01********
    serial_port_settings:
      ssh_authorization: INSTANCE_METADATA
    gpu_settings: {}
    fqdn: epdv79cu67np********.auto.internal
    scheduling_policy: {}
    network_settings:
      type: STANDARD
    placement_policy: {}
    

    For more information about the yc compute instance create command, see the CLI reference.

    To create a VM, use the create REST API method for the Instance resource or the InstanceService/Create gRPC API call.

    As a result, you will have the mywebserver VM in your folder. To connect to the VM over SSH, use the yc-user username and the public IP address of the VM. If you intend to use the web server you created over a long period of time, makeconvert ’its public IP address to static.

  2. Create and activate the virtual environment.

    A virtual environment provides an isolated space for Python projects on the server. All required dependencies, e.g., executables, libraries, and other files, are copied to the selected folder, and the application uses them rather than those installed on the system. This both ensures the development environment is stable and keeps the base system clean.

    To create a virtual environment, use the Python 3 venv library module:

    1. Connect to mywebserver.

    2. In the current user's directory, create a project subdirectory named flask_blog where the application will be stored and navigate to it:

      mkdir flask_blog && cd flask_blog
      
    3. Install the venv virtualization module:

      sudo apt install python3.10-venv
      
    4. Create the env virtual environment:

      python3 -m venv env
      
    5. Activate the virtual environment:

      source env/bin/activate
      

      After you activate the virtual environment, a prefix with the environment name will appear in the command line:

      (env) yc-user@ubuntu:~/flask_blog$
      

      Note

      To effectively track and manage project development, you can use a version control system. In this case, add the env directory to the .gitignore file to only track files associated with the project.

      To deactivate the virtual environment, run this command:

      deactivate
      
  3. Install Flask:

    pip install flask
    

    Result:

    Successfully installed Jinja2-3.1.3 MarkupSafe-2.1.5 Werkzeug-3.0.1 blinker-1.7.0 click-8.1.7 flask-3.0.2 itsdangerous-2.1.2
    

Create and run the application

Create a simple web application inside a Python file and run it to launch the server.

  1. In the flask_blog project directory, create and open the app.py file:

    nano app.py
    

    This file enables you to understand how the application will handle HTTP requests.

  2. Add the following code to the file:

    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def hello():
        return 'Hello, World!'
    

    In this code:

    • You import the Flask object from the flask package.
    • You create an instance of the Flask application named app. The special __name__ variable contains the name of the current Python module and points its location to the instance. The reason this is required is that Flask creates several routes within the application.
    • The @app.route('/') decorator transforms the Python function into a Flask view function. The view function converts the return value into an HTTP response that can be processed by an HTTP client, such as a web browser. The '/' value in @app.route()​​ means that this function will respond to web requests for the / URL, which is the primary URL of the web application.
    • You create a hello() function that returns the Hello, World! string as a response.

    Save and close the app.py file.

  3. Set the Flask environment variables:

    export FLASK_APP=app && export FLASK_DEBUG=true
    

    Where:

    • FLASK_APP=app points to the app location, i.e., the app.py file.
    • FLASK_DEBUG=true means the application should run in development mode.
  4. Run the application:

    flask run \
      --host=0.0.0.0
    

    Where the --host=0.0.0.0 parameter starts the application server on all IP addresses of the VM. Failure to specify this parameter will prevent you from accessing the application using the VM public IP address.

    Result:

     * Serving Flask app 'app'
     * Debug mode: on
    WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
     * Running on all addresses (0.0.0.0)
     * Running on http://127.0.0.1:5000
     * Running on http://192.168.1.14:5000
    Press CTRL+C to quit
     * Restarting with stat
     * Debugger is active!
     * Debugger PIN: 884-371-***
    

    You will need the following information:

    • Name of the running application: app.
    • Debug mode: on: Means that the Flask debugger is active. This feature is useful in development as it provides detailed error messages whenever issues occur, making it easier to resolve them.
    • The application is running on all addresses.

    You can change the web server port to any other value by setting this optional parameter: --port=<port_number>. For example, the flask run --host=0.0.0.0 --port=5001 command will start the Flask web server on port 5001.

  5. In you browser address bar, enter http://<public_IP_address_of_VM>:5000/.

    The browser will display the Hello, World! string. This means that your application is running properly and can be accessed from the internet.

Warning

Flask uses a simple web server to maintain the application in the development environment. It is reserved for testing and debugging and should not be used in the production environment. For more information, see this Flask article.

Keep the web server running in the current window and open a new terminal window. You will use this new window to finish setting up your application. This way, you can test your changes without having to continuously stop and restart the web server.

When you open a new terminal window:

  1. Connect to the VM.

  2. Go to the flask_blog project directory, activate the virtual environment, and set the environment variables:

    cd flask_blog && source env/bin/activate && export FLASK_APP=app && export FLASK_DEBUG=true
    

Create and configure the HTML templates

At this point, the application can only display a plain text message in the browser. To display the data and controls the user needs, add HTML templates to your application.

To use templates in the app, you can use the render_template() helper function enabled by the Jinja template engine. You will use HTML templates to create application pages: the main page that lists blog posts and the pages for creating, viewing, and editing a post.

Create the main page HTML template

  1. Open the app.py file:

    nano app.py
    
  2. Replace the file contents with this code:

    from flask import Flask, render_template
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        return render_template('index.html')
    

    In this code:

    • You import the render_template() helper function to render HTML templates.
    • The hello() function is replaced with the index() function that returns the result of invoking the render_template() helper function with the index.html​​ argument. The function argument references a template file in the template directory that will be used for rendering.

    Note

    You will create the template directory and the index.html file later. Running the application at this stage will produce an error.

    Save and close the app.py file.

  3. In the flask_blog project directory, create a subdirectory named templates:

    mkdir templates
    
  4. Create and open the index.html template file in the templates directory:

    nano templates/index.html
    
  5. Add the following code to the index.html file:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>FlaskBlog</title>
      </head>
      <body>
        <h1>Welcome to FlaskBlog</h1>
      </body>
    </html>
    

    This HTML code describes a simple page with the FlaskBlog header and Welcome to FlaskBlog as the first level header.

    Save and close the index.html file.

  6. Refresh the main page of your application, http://<VM_public_IP_address>:5000/.

    Result:

    Welcome to FlaskBlog.
    
  7. In addition to the templates directory, Flask web applications typically have a static directory that stores static files, such as CSS styles, JavaScript scripts, or images used by the application.

    In the flask_blog project directory, create a subdirectory named static:

    mkdir static
    
  8. In the static directory you just created, create the css subdirectory:

    mkdir static/css
    

    Note

    Subdirectories are used to organize static files in dedicated folders. For example, the js directory usually stores JavaScript files, the images (or img) directory stores images, etc.

  9. In the static/css project directory, create and open the style.css file:

    nano static/css/style.css
    
  10. Add this code to the style.css file:

    h1 {
        border: 2px #eee solid;
        color: #fc3d17;
        text-align: center;
        padding: 10px;
    }
    

    This CSS code changes the formatting of the text wrapped by the <h1> HTML tags:

    • border: Adds a border.
    • color: Changes the text color.
    • text-align: Aligns the text.
    • padding: Adds a padding between the element's border and text.

    Save and close the style.css file.

  11. Open the index.html template:

    nano templates/index.html
    
  12. Add a link to style.css inside the <head> section:

    ...
      <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="{{ url_for('static', filename= 'css/style.css') }}">
        <title>FlaskBlog</title>
      </head>
    ...
    

    The link uses the url_for() helper function​​​ to generate the URL for the file location. The first argument means the link references a static file; the second argument specifies the path to the file in the static file subdirectory.

    Save and close the index.html file.

  13. Refresh the main page of your application, http://<public_IP_address_of_VM>:5000/.

    You will see that the Welcome to FlaskBlog​​​ text changed its color to red and is now centered inside the frame.

The CSS language enables you to create a consistent and unique design for your web application. If you are not familiar with CSS, use the Bootstrap toolkit that provides simple and easy-to-use UI components. In this tutorial, you will use Bootstrap.

Configure other HTML templates

In other HTML templates, you will need to reuse most of the HTML code from the index.html template. To avoid unnecessary duplicate code, use the base template file as a parent for all other HTML templates in the project. For more information, see this Jinja article.

  1. Create and open the base.html base template in the templates directory:

    nano templates/base.html
    
  2. Add the following code to the file:

    <!doctype html>
    <html lang="en">
      <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    
        <!-- Bootstrap CSS -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
    
        <title>{% block title %} {% endblock %}</title>
      </head>
      <body>
        <nav class="navbar navbar-expand-lg bg-light">
          <div class="container-fluid">
            <a class="navbar-brand" href="{{ url_for('index')}}">FlaskBlog</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
              <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <li class="nav-item">
                  <a class="nav-link active" href="#">About</a>
                </li>
              </ul>
            </div>
          </div>
        </nav>
        <div class="container">
          {% block content %} {% endblock %}
        </div>
    
        <!-- Optional JavaScript -->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
      </body>
    </html>
    

    This file contains HTML code as well as additional code required by Bootstrap. The <meta> tags contain browser information, the <link> tag binds Bootstrap CSS files, and the <script> tag references a JavaScript-based script that enables additional Bootstrap features. For more information, see the Bootstrap documentation.

    The following code elements are specific to the Jinja template engine:

    • {% block title %} {% endblock %}: Placeholder section for a header. You will use it in other templates to give a custom header for each page in your application without rewriting the <head> section.
    • {{ url_for('index')}}: Calls a function that returns the URL for the index() view function. This call is different from url_for() that we used previously to link the CSS file. In this case, you need to provide only one argument: the name of the view function linked to its associated route rather than a file.
    • {% block content %} {% endblock %}: Section that will contain content specific to each child template.

    Save and close the base.html file.

  3. Configure your HTML templates to inherit code from the base template. Open the index.html template:

    nano templates/index.html
    
  4. Replace the file contents with this code:

    {% extends 'base.html' %}
    
    {% block title %} Welcome to FlaskBlog {% endblock %}
    
    {% block content %}
      <h1> Welcome to FlaskBlog </h1>
    {% endblock %}
    

    In this code:

    • {% extends 'base.html' %}: Refers to inheritance from the base.html template.
    • {% block title %} ... {% endblock %}: Specifies a header that will replace the variable in the appropriate block of the base.html parent template.
    • {% block content %} ... {% endblock %}: Specifies the content that will replace the variable in the appropriate block of the base.html parent template.

    Since Welcome to FlaskBlog is used as both the page name and the header that appears below the navigation bar, you can make this code even more concise and avoid duplication:

    {% extends 'base.html' %}
    
    {% block content %}
      <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1>
    {% endblock %}
    

    Save and close the index.html file.

  5. Refresh the main page of your application, http://<public_IP_address_of_VM>:5000/.

    Your page will now have a navigation bar and a styled header.

Configure the database

Create a database to store blog posts and populate it with sample entries.

You will use SQLite to work with the web application data, since the sqlite3 module, which you will need to work with the database, comes bundled with Python.

Prepare the schema.sql file containing SQL commands to create a database consisting of a table named posts with multiple columns.

  1. In the flask_blog project directory, create and open the schema.sql file:

    nano schema.sql
    
  2. Add the following code to the file:

    DROP TABLE IF EXISTS posts;
    
    CREATE TABLE posts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        title TEXT NOT NULL,
        content TEXT NOT NULL
    );
    

    In this code:

    • The DROP TABLE IF EXISTS posts; command deletes the posts table if it already exists. To prevent possible errors, we recommend performing this step each time you create a new table.
    • The CREATE TABLE posts command creates the posts table with the following columns:
      • id: Integer that is the PRIMARY KEY of the table. AUTOINCREMENT means that its value will be a unique number generated automatically when inserting a new record (blog post) into the table.
      • created: New record timestamp. NOT NULL means that the value in the column cannot be empty. DEFAULT CURRENT_TIMESTAMP​​​ sets the default value as the time when a new record is added to the table. This means that you do not need to manually specify the time when a post is created; it will be added automatically.
      • title: Post title.
      • content: Body text.

    Save and close the schema.sql file.

    Note

    When using these SQL commands, any previous contents of the posts table in the database.db file will be deleted. Do not save anything important into the database through the web application until you have completed this tutorial.

  3. In the flask_blog project directory, create and open the init_db.py file:

    nano init_db.py
    

    This file will create the SQLite database file for your web application.

  4. Add the following code to the file:

    import sqlite3
    
    connection = sqlite3.connect('database.db')
    
    
    with open('schema.sql') as f:
        connection.executescript(f.read())
    
    cur = connection.cursor()
    
    cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)",
                ('First Post', 'Content for the first post')
                )
    
    cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)",
                ('Second Post', 'Content for the second post')
                )
    
    connection.commit()
    connection.close()
    

    In this code:

    • You import the sqlite3 module.
    • You open the connection to the database.db file, created once you run the Python file.
    • The open() function opens the previously created schema.sql SQL schema.
    • The executescript() method formalizes the contents of the schema by running multiple SQL statements at once and creating the posts table.
    • The cursor() method opens the cursor for sending queries to the database.
    • The execute() method executes two INSERT SQL statements, adding two sample posts to the posts table.
    • These changes are committed and the connection is closed.

    Save and close the init_db.py file.

  5. Run the file using python:

    python init_db.py
    
  6. Make sure the database.db file is in the flask_blog project directory:

    ls -l
    

    Result:

    total 40
    -rw-rw-r-- 1 yc-user yc-user   105 Mar 23 19:45 app.py
    -rw-r--r-- 1 yc-user yc-user 12288 Mar 24 12:27 database.db
    drwxrwxr-x 5 yc-user yc-user  4096 Mar 23 19:30 env
    -rw-rw-r-- 1 yc-user yc-user   472 Mar 24 12:27 init_db.py
    drwxrwxr-x 2 yc-user yc-user  4096 Mar 23 19:45 __pycache__
    -rw-rw-r-- 1 yc-user yc-user   204 Mar 24 12:26 schema.sql
    drwxrwxr-x 2 yc-user yc-user  4096 Mar 24 12:29 static
    drwxrwxr-x 2 yc-user yc-user  4096 Mar 24 12:30 templates
    

Display blog posts

On the main page of your application, configure the display of the blog posts list and create a page to view individual posts by their ID.

Display the blog posts list

Update the index() view function and the index.html template to display the list of all posts stored in the database.

  1. Open the app.py file:

    nano app.py
    
  2. Replace the file contents with this code:

    import sqlite3
    from flask import Flask, render_template
    
    def get_db_connection():
        conn = sqlite3.connect('database.db')
        conn.row_factory = sqlite3.Row
        return conn
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        conn = get_db_connection()
        posts = conn.execute('SELECT * FROM posts').fetchall()
        conn.close()
        return render_template('index.html', posts=posts)
    

    Additionally, this code does the following:

    • Imports the sqlite3 module.
    • Creates the get_db_connection() function, which, in turn:
      • Creates the conn object for connecting to database.db.
      • Assigns database data to the row_factory attribute of the conn object.
      • Returns the conn object, which will be used to access the database.
    • The updated index() function:
      • Assigns the database connection object returned by get_db_connection() to conn.
      • Uses the fetchall() method to assign the posts object the result of the SQL query for selecting all records from the posts table.
      • Uses the close() method to close the connection to the database.
      • Returns the HTML code of the application main page, formed from the index.html template, which gets the posts object containing all records of the table with posts as an argument.

    Save and close the app.py file.

  3. Open the index.html main page template:

    nano templates/index.html
    
  4. Replace the file contents with this code:

    {% extends 'base.html' %}
    
    {% block content %}
      <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1>
      {% for post in posts %}
        <a href="#">
          <h2>{{ post['title'] }}</h2>
        </a>
        <span class="badge text-bg-primary">{{ post['created'] }}</span>
        <hr>
      {% endfor %}
    {% endblock %}
    

    This code adds:

    • {% for post in posts %} ... {% endfor %}: Jinja loop, which displays, one by one, each element contained in the posts object provided by the index() function in app.py. Inside this loop, the post title with a link is displayed in the <h2>​​ HTML header for each post. Later on, these links will be used to open a view page for each post.
    • {{ post['title'] }}: Post title from the post variable, i.e., the element of the posts object processed in the current iteration of the for loop.
    • Similarly, the post['created'] date is displayed with the badge CSS class applied to it.

    Save and close the index.html file.

  5. Refresh the main page of your application, http://<public_IP_address_of_VM>:5000/.

    You will see a list of the two post titles you added to the database when creating it.

Display individual blog posts

Create a new Flask route with a view function that will process a new HTML template to display an individual blog post by its ID. The application will use the new route to display the post at http://<public_IP_address_of_VM>:5000/ID with the appropriate ID, provided such an ID exists in the database.

  1. Open the app.py file:

    nano app.py
    
  2. Replace the file contents with this code:

    import sqlite3
    from flask import Flask, render_template
    from werkzeug.exceptions import abort
    
    def get_db_connection():
        conn = sqlite3.connect('database.db')
        conn.row_factory = sqlite3.Row
        return conn
    
    def get_post(post_id):
        conn = get_db_connection()
        post = conn.execute('SELECT * FROM posts WHERE id = ?',
                            (post_id,)).fetchone()
        conn.close()
        if post is None:
            abort(404)
        return post
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        conn = get_db_connection()
        posts = conn.execute('SELECT * FROM posts').fetchall()
        conn.close()
        return render_template('index.html', posts=posts)
    
    @app.route('/<int:post_id>')
    def post(post_id):
        post = get_post(post_id)
        return render_template('post.html', post=post)
    

    Additionally, this code does the following:

    • Imports the abort() function from the Werkzeug library. It allows you to generate a Flask response with the 404 Not Found​​​ message if the blog post with the ID specified in the URL does not exist.
    • Creates the get_post() function that takes the post_id argument. This function:
      • Assigns the database connection object returned by get_db_connection() to conn.
      • Uses the fetchone() method to assign the post object the result of the SQL query which retrieves the blog post string and its ID matching the post_id value from the database.
      • Uses the close() method to close the connection to the database.
      • Uses the abort() function to respond with the 404 error code if the post with the ID provided to post_id is not found; following that, the function terminates.
      • Returns the post object containing the database table row for this post if the post with the ID provided to post_id is found.
    • Creates the post(post_id) view function. This function:
      • Uses the decorator to add the <int:post_id> variable rule specifying that the URL after / is a positive integer (marked with the int converter) used by the view function.
      • Assigns the post object the contents of the database table row matching the blog post with the specified ID using the get_post(post_id) function.
      • Returns the HTML code of the post view page formed from the post.html template, which gets the post object containing the post contents as an argument.

    Save and close the app.py file.

  3. Create and open the post.html template file in the templates directory:

    nano templates/post.html
    
  4. Add the following code to the file:

    {% extends 'base.html' %}
    
    {% block content %}
        <h2>{% block title %} {{ post['title'] }} {% endblock %}</h2>
        <p>{{ post['content'] }}</p>
        <span class="badge text-bg-primary">{{ post['created'] }}</span>
    {% endblock %}
    

    This code is similar to that of the index.html template. The difference is that it does not use the for loop here since the template should display a single post with its contents specified in post['content'].

    Save and close the post.html file.

  5. To view the first two posts you created together with the database, open these URLs: http://<public_IP_address_of_VM>:5000/1 and http://<public_IP_address_of_VM>:5000/2.

    If you try to open http://<public_IP_address_of_VM>:5000/3, you will see an error page stating that the requested blog post is not found.

  6. Add links from the post titles that open the post view pages to the application main page.

    Open the index.html template:

    nano templates/index.html
    
  7. Replace the # value of the href attribute with {{ url_for('post', post_id=post['id']) }} so that the for loop is as follows:

    ...
      {% for post in posts %}
        <a href="{{ url_for('post', post_id=post['id']) }}">
          <h2>{{ post['title'] }}</h2>
        </a>
        <span class="badge text-bg-primary">{{ post['created'] }}</span>
        <hr>
      {% endfor %}
    ...
    

    The url_for() function takes these arguments:

    • 'post': post() view function.
    • post_id: Variable with the post['id'] value used by the view function.

    The post['id'] function returns a valid URL for each post title based on the post ID.

    Save and close the index.html file.

  8. Go to the application main page, http://<public_IP_address_of_VM>:5000/, and make sure the post titles are valid links to the posts.

Add post actions

Enable users to create, edit, and delete posts in your application.

Creating a new post

Create a page where the user can add a new post with a title.

  1. Open the app.py file:

    nano app.py
    
  2. Replace the file contents with this code:

    import sqlite3
    from flask import Flask, render_template, request, url_for, flash, redirect
    from werkzeug.exceptions import abort
    
    def get_db_connection():
        conn = sqlite3.connect('database.db')
        conn.row_factory = sqlite3.Row
        return conn
    
    def get_post(post_id):
        conn = get_db_connection()
        post = conn.execute('SELECT * FROM posts WHERE id = ?',
                            (post_id,)).fetchone()
        conn.close()
        if post is None:
            abort(404)
        return post
    
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'your secret key'
    
    @app.route('/')
    def index():
        conn = get_db_connection()
        posts = conn.execute('SELECT * FROM posts').fetchall()
        conn.close()
        return render_template('index.html', posts=posts)
    
    @app.route('/<int:post_id>')
    def post(post_id):
        post = get_post(post_id)
        return render_template('post.html', post=post)
    
    @app.route('/create', methods=('GET', 'POST'))
    def create():
        return render_template('create.html')
    

    Additionally, this code does the following:

    • Imports these items:
      • request global object to access the data provided through the HTML form.
      • url_for() function to generate URLs.
      • flash() function to display a message when processing a request.
      • redirect() function to redirect the client to a different page.
    • Using the app.config object, adds the SECRET_KEY configuration required for the flash() function to store pop-up messages in the client's browser session. The secret key is a long string of random characters. It is used to create secure sessions and allows Flask to remember information between requests. For example, it can go from the post creation page to the main application page. A user can access the information stored in a session but cannot edit it without the secret key. This is why you should never give anyone access to your secret key. For more information, see this Flask article.
    • Uses the create() view function to return the HTML code of the post creation page generated from the create.html template. The decorator creates the /create route that accepts both GET and POST requests. By default, only GET requests are accepted. To ensure the route also accepts POST requests, which are used by the browser when sending form data, you need to provide a tuple of valid request types in the methods argument.

    Save and close the app.py file.

  3. Create and open the create.html template file in the templates directory:

    nano templates/create.html
    
  4. Add the following code to the file:

    {% extends 'base.html' %}
    
    {% block content %}
    <h1>{% block title %} Create a New Post {% endblock %}</h1>
    
    <form method="post">
      <div class="mb-3">
        <label for="title" class="col-sm-2 col-form-label">Title</label>
        <input type="text" name="title"
               placeholder="Post title" class="form-control"
               value="{{ request.form['title'] }}">
      </div>
    
      <div class="mb-3">
        <label for="content" class="col-sm-2 col-form-label">Content</label>
        <textarea name="content" placeholder="Post content"
                  class="form-control" rows="3">{{ request.form['content'] }}</textarea>
      </div>
      <div class="mb-3">
        <button type="submit" class="btn btn-primary">Submit</button>
      </div>
    </form>
    {% endblock %}
    

    Basically, this is plain HTML code that will display fields for the post title and body as well as a button for submitting the form.

    The post title value is stored in the {{ request.form['title'] }} object, and the content value, in the {{ request.form['content'] }} object. This way, the data you enter will not get lost should something go wrong. For example, if you add the body text of a post but forget to add a title, you will see a message saying the title is a required field. However, you will not lose the body text, as it will be saved in the request global object you have access to in your templates.

    Save and close the create.html file.

  5. Go to http://<public_IP_address_of_VM>:5000/create and make sure you can see the Create a New Post page with input fields for the post title and contents as well as the submit form button.

    The form on this page sends a POST request to the create() view function. However, the function currently lacks the code to handle the POST request, so the input data will not be saved into the database after the form is submitted. You can fix this by updating the create() view function.

  6. Open the app.py file:

    nano app.py
    
  7. Replace the contents of the create() view function with the code below:

    ...
    @app.route('/create', methods=('GET', 'POST'))
    def create():
        if request.method == 'POST':
            title = request.form['title']
            content = request.form['content']
    
            if not title:
                flash('Title is required!')
            else:
                conn = get_db_connection()
                conn.execute('INSERT INTO posts (title, content) VALUES (?, ?)',
                             (title, content))
                conn.commit()
                conn.close()
                return redirect(url_for('index'))
    
        return render_template('create.html')
    

    This function code:

    • Employs the if request.method == 'POST' condition to make sure the code is only executed for POST requests.
    • Retrieves the new post title and contents submitted in the form fields from the request.form global object which contains the form data.
    • If no post title is specified in the form, the if not title condition will be met and the user will see a message instructing them to fill in the title field.
    • If the title is specified:
      • The get_db_connection() function opens a connection to the database and a new post record is added to the posts table, containing the post title and text received through the form.
      • The changes are committed and the connection is closed.
      • The redirect() function redirects the user to the application main page.

    Save and close the app.py file.

  8. Open the http://<public_IP_address_of_VM>:5000/create URL, enter the post title and contents, and submit the form. After submitting the form, you will be redirected to the application main page, where you will see a list of all posts, including the new one.

  9. In the base.html template, enable the display of a pop-up message and add a link to the new post form to the navigation bar. Open the base.html template:

    nano templates/base.html
    
  10. Prepend the <li> tag to the About link inside <nav>:

    ...
        <nav class="navbar navbar-expand-lg bg-light">
          <div class="container-fluid">
            <a class="navbar-brand" href="{{ url_for('index')}}">FlaskBlog</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
              <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <li class="nav-item">
                  <a class="nav-link active" href="{{url_for('create')}}">New Post</a>
                </li>
                <li class="nav-item">
                  <a class="nav-link active" href="#">About</a>
                </li>
              </ul>
            </div>
          </div>
        </nav>
    ...
    
  11. Under <div class="container"> , add the for loop before the content section to display pop-up messages under the navigation bar:

    ...
        <div class="container">
          {% for message in get_flashed_messages() %}
            <div class="alert alert-danger">{{ message }}</div>
          {% endfor %}
          {% block content %} {% endblock %}
        </div>
    ...
    

    Pop-up messages are available in the special get_flashed_messages() Flask function.

    Save and close the base.html file.

    The New Post control linked to the /create route will appear on the navigation bar.

  12. Go to the application main page, http://<public_IP_address_of_VM>:5000, and make sure you can create a new post using the New Post link on the navigation bar.

Editing an existing post

Create a page where you can edit an existing post.

  1. Open the app.py file:

    nano app.py
    
  2. Add the edit() view function at the end of the file:

    ...
    @app.route('/<int:id>/edit', methods=('GET', 'POST'))
    def edit(id):
        post = get_post(id)
    
        if request.method == 'POST':
            title = request.form['title']
            content = request.form['content']
    
            if not title:
                flash('Title is required!')
            else:
                conn = get_db_connection()
                conn.execute('UPDATE posts SET title = ?, content = ?'
                            ' WHERE id = ?',
                            (title, content, id))
                conn.commit()
                conn.close()
                return redirect(url_for('index'))
    
        return render_template('edit.html', post=post)
    

    Editing an existing post is much like creating a new one, so this view function is almost identical to the create() function. The post to edit is specified in the URL, with the post ID provided to the edit() function in the id argument. The get_post() function also gets the same value so it can retrieve the current contents of the post from the database. The application receives the updated data in a POST request, which is processed in the if request.method == 'POST' condition.

    As with creating a new post, the function extracts the data from the request.form object into separate variables and then checks whether the user filled in the post title. If the title is missing, the function displays a pop-up message saying this is a required field. Otherwise, the function opens a connection to the database and updates the fields in the row with the required post in the posts table. The post ID in the database matches the ID specified in the URL.

    When handling a GET request, the function returns the HTML code generated from the edit.html template with the post title and text values from the post object.

    Save and close the app.py file.

  3. Create and open the edit.html template file in the templates directory:

    nano templates/edit.html
    
  4. Add the following code to the file:

    {% extends 'base.html' %}
    
    {% block content %}
    <h1>{% block title %} Edit "{{ post['title'] }}" {% endblock %}</h1>
    
    <form method="post">
      <div class="mb-3">
        <label for="title" class="col-sm-2 col-form-label">Title</label>
        <input type="text" name="title" placeholder="Post title"
               class="form-control"
               value="{{ request.form['title'] or post['title'] }}">
      </div>
    
      <div class="mb-3">
        <label for="content" class="col-sm-2 col-form-label">Content</label>
        <textarea name="content" placeholder="Post content"
                  class="form-control" rows="3">{{ request.form['content'] or post['content'] }}</textarea>
      </div>
      <div class="mb-3">
        <button type="submit" class="btn btn-primary">Submit</button>
      </div>
    </form>
    <hr>
    {% endblock %}
    

    The code is similar to the create.html template code, except for the {{ request.form['title'] or post['title'] }} and {{ request.form['content'] or post['content'] }} expressions. These expressions display the data stored in the request, or if the request does not exist, the data from the post variable provided to the template with the current database values.

    Save and close the edit.html file.

  5. Open http://<public_IP_address_of_VM>:5000/1/edit to edit the first post. You will see the Edit First Post page with the post details. Edit the post, submit the form, and make sure the post is updated.

  6. On the main application page, add a link to the edit page for each post in the list. Open the index.html template:

    nano templates/index.html
    
  7. Add a link to the edit post page after its creation date:

    ...
      {% for post in posts %}
        <a href="{{ url_for('post', post_id=post['id']) }}">
          <h2>{{ post['title'] }}</h2>
        </a>
        <span class="badge text-bg-primary">{{ post['created'] }}</span>
        <a href="{{ url_for('edit', id=post['id']) }}">
          <span class="badge text-bg-warning">Edit</span>
        </a>
        <hr>
      {% endfor %}
    ...
    

    This adds the Edit link to the post titles on the main page. This link points to the edit() view function and provides it with the ID from post['id'].

    Save and close the index.html file.

  8. Go to the application main page, http://<public_IP_address_of_VM>:5000, and make sure it now has Edit links.

Deleting a post

  1. Open the app.py file:

    nano app.py
    
  2. Add the delete() view function at the end of the file:

    ...
    @app.route('/<int:id>/delete', methods=('POST',))
    def delete(id):
        post = get_post(id)
        conn = get_db_connection()
        conn.execute('DELETE FROM posts WHERE id = ?', (id,))
        conn.commit()
        conn.close()
        flash('"{}" was successfully deleted!'.format(post['title']))
        return redirect(url_for('index'))
    

    This function only accepts POST requests. This means that going to http://<public_IP_address_of_VM>:5000/ID/delete in a browser will return an error, as web browsers use GET requests by default. You can only send a POST request through this route by using a form that provides the ID of the post you want to delete.

    After receiving a POST request, the function opens a connection to the database and runs the DELETE FROM SQL command. Following that, the changes are committed and the connection is closed. The application redirects the user to the main page and displays a message that the system deleted the post.

    The Delete button will be added to the post edit page, so you do not need a separate template for this feature.

    Save and close the app.py file.

  3. Open the edit.html template:

    nano templates/edit.html
    
  4. Add the <form> tag right after the <hr> tag and before the {% endblock %} line:

    ...
    <hr>
    <form action="{{ url_for('delete', id=post['id']) }}" method="POST">
      <input type="submit" value="Delete Post"
             class="btn btn-danger btn-sm"
             onclick="return confirm('Are you sure you want to delete this post?')">
    </form>
    {% endblock %}
    

    This code uses the confirm() method to ask the user to confirm their action before sending a request.

    Save and close the edit.html file.

  5. Go to the application main page, http://<public_IP_address_of_VM>:5000, and make sure the post edit page now has the Delete Post button.

Your application is now ready. If you did everything correctly, the final source code of your project should match the code on this page.

Summarize the results

In this tutorial, you created a simple web application using Flask, SQLite Werkzeug, Jinja, and Bootstrap. The result is a fully functional blog that allows you to create, read, edit, and delete posts using URL parameters and HTML forms.

You can take this project further with a wide range of community-created Flask extensions, e.g.:

  • Flask-Login: Allows you to manage user sessions, i.e., handles login/logout tasks and user session management.
  • Flask-SQLAlchemy: Streamlines the use of Flask by supporting SQLAlchemy. It is a Python SQL toolkit and Object Relational Mapper designed for working with databases.
  • Flask-Mail: Helps you send emails from Flask applications.

Delete the resources you created

To stop paying for the deployed web server, delete the VM.

If you reserved a static public IP address for your web server, delete it.

Was the article helpful?

Previous
Terraform
Next
Hosting a static Gatsby website
© 2025 Direct Cursus Technology L.L.C.