Skip to main content

How to Build a Flask CKEditor Application with PostgreSQL and Docker?


In our previous tutorial, we learned how to set up PgAdmin and PostgreSQL using Docker containers, providing a convenient and portable database management environment. Building on that foundation, this tutorial takes the next step by integrating PostgreSQL with a Flask web application and adding rich text editing capabilities using CKEditor. This tutorial demonstrates a real-world use case that further enhances the value of the PgAdmin and PostgreSQL setup from the previous tutorial.

Prerequisite:

This tutorial is part of the Flask CKEditor Project Series.

📚 View the Complete Flask CKEditor Series

⬅ Previous Part


Preliminary:

I need to set up the file and folder structure, as below:
Everything is as in the previous tutorial, except that I will create a new "docker-compose.yaml", "dockerfile" and "requirements.txt" file. Since this tutorial uses PostgreSQL as the database, I also amended both app.py and database.py.

I have listed all the dependencies in requirements.txt, and therefore, it will install all of them.

What are those files?
1) docker-compose.yaml—a configuration file used to define and run multi-container Docker applications. In my use case, I have 3 services, including PostgreSQL, PgAdmin, and a Flask app.

2) dockerfilea text document containing all the commands a user could call on the command line to assemble a single Docker image. In my use case, I have described how the Flask installation works.

3) requirements.txtlists all the external packages (and their specific versions) that I need to run. I will discuss it later on.

What are the scenarios for running a Flask app with PostgreSQL?
(a) Run on a local computer without Docker.
In fact, I can run any Flask app with PostgreSQL without Docker; simply download PgAdmin and PostgreSQL in the desktop version, and they work well.

(b) Run on Docker and isolated from the local computer
Since I have discussed Docker with PgAdmin and PostgreSQL in the last tutorial, I will further discuss how to use it with a Flask app.


Step 1: Launch Docker Desktop and configure the relevant file
First and foremost, I need to launch Docker Desktop on my local computer, and then I will configure the following file.
(a) docker-compose.yaml
services:
  postgres:
    image: postgres:16
    container_name: postgres_db
    restart: always
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    restart: always
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: admin123
    ports:
      - "8080:80"

  flask:
    build: . 
    container_name: flask_app
    restart: always
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://admin:secret@postgres:5432/mydb
    depends_on:
      - postgres

volumes:
  postgres_data: {} 
In the previous tutorial, I discussed PostgreSQL and PgAdmin, while in this tutorial, I simply added Flask to the same YAML file. Since Docker is separated from my local computer, if the Flask app runs outside the Docker container, the Flask app will not be able to access PostgreSQL. Therefore, I need to include the same Docker to facilitate communication between them. 

(b) Dockerfile
# 1. Use an official lightweight Python image
FROM python:3.11-slim

# 2. Set the working directory inside the container
WORKDIR /app

# 3. Copy the requirements file first (helps with Docker caching)
COPY requirements.txt .

# 4. Install the Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# 5. Copy the rest of your Flask application code
COPY . .

# 6. Expose the port Flask runs on
EXPOSE 5000

# 7. Command to run the application (using Gunicorn for production)
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
The above Dockerfile describes step-by-step how to set up a Flask image, the working path, where to locate the dependencies file, and the exposed port.

(c) Requirements.txt
Flask
Flask-WTF
Flask-CKEditor
psycopg2-binary
WTForms
email-validator
gunicorn
It is one of the requirements (#4) in the Dockerfile; therefore, I have listed all the Python packages to install on Docker. To connect to PostgreSQL, I have to install psycopg2-binary.


Step 2: Amend database.py and app.py
Once I have installed psycopg2-binary, I will connect them both in database.py and app.py. All the content remains the same, except for the following:
(a) database.py
import os
import psycopg2
import time

def init_db():
    DATABASE_URL = os.environ.get(
        'DATABASE_URL', 
        'postgresql://admin:secret@postgres:5432/mydb'
    )
    
    # Retry mechanism: Give Postgres a few seconds to fully start up inside Docker
    for i in range(5):
        try:
# Connect to PostgreSQL URL conn = psycopg2.connect(DATABASE_URL) c = conn.cursor() # Create table query c.execute(""" CREATE TABLE IF NOT EXISTS message ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, subscribe BOOLEAN DEFAULT FALSE, message TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) conn.commit() c.close() conn.close() break except psycopg2.OperationalError as e: print(f"Database not ready yet (attempt {i+1}/5), waiting...") time.sleep(3)
I have created an init_db() function; this will call it up again in the app.py. This is how I connect the database.py and app.py together.

I have specified the URL 'postgresql://admin:secret@postgres:5432/mydb' for the Flask container under the Docker YAML file; therefore, when I need to connect them, I will use the same database URL, so Docker will know where to locate it. 

The column is as follows:
(i) id                   - series, automatically created
(ii) name           - varchar, from the web form
(iii) email           - varchar, from the web form
(iv) subscribe   - boolean, from the web form
(v) message     - text, from the web form
(vi) timestamp - timestamp, automatically created

If the connection fails, it will restart 5 times until the database is connected.

(b) app.py
from flask import (Flask, render_template, redirect, 
                   url_for)
from database import init_db
import psycopg2

init_db()

# Define the route for handling contact form submissions (supports both GET 
# and POST requests)
@app.route('/message', methods=['GET', 'POST'])
def submit():
    # Instantiate the Flask-WTF contact form
    form = ContactForm()

    # Check if the request is a POST request and if all form validation
    # rules pass
    if form.validate_on_submit():
        try:
            print("Testing database connection...")

            # Establish a connection to the PostgreSQL database container
            conn = psycopg2.connect(
                host="postgres",    # Name of the PostgreSQL service/container
                port=5432,          # Default PostgreSQL port
                user='admin',       # Database username
                password='secret',  # Database password
                dbname="mydb"       # Target database name
            )
            print("Connected!")

            # Create a cursor object to execute SQL commands
            c = conn.cursor()

            # Parameterized SQL query to prevent SQL injection vulnerabilities
            query = """
                INSERT INTO message (name, email, subscribe, message)
                VALUES (%s, %s, %s, %s)
                """

            # Map form data into a tuple matching the query parameters
            # PostgreSQL requires an explicit boolean type, handled here by bool()
            data = (
                form.name.data,
                form.email.data,
                bool(form.subscribe.data),
                form.message.data
            )

            # Execute the query with the sanitized data
            c.execute(query, data)
            
            # Commit the transaction to save changes permanently to the database
            conn.commit()

            # Clean up and close database resources
            c.close()
            conn.close()

            # Redirect the user to the 'thankyou' endpoint upon successful 
            # submission
            return redirect(url_for('thankyou'))

        except Exception as e:
            # Capture and print the full error traceback for easier debugging
            import traceback
            print(traceback.format_exc())
            
    # If it's a GET request or form validation fails, render the initial 
    # form page
    return render_template('contact.html', form=form)
    
if __name__ == '__main__':
   app.run(host='0.0.0.0', port=5000, debug=True)
Above is an excerpt from app.py. Since the 'index' and 'thankyou' functions remain the same, they will not be displayed here. Under the templates folder, the contacts.html and thankyou.html also remain intact; please refer to the previous tutorial on CKEditor on SQLite. 

PostgreSQL connection
Since I had installed psycopg2-binary; it is used to connect to the PostgreSQL database. My host is 'postgre', which is the service name in the 'docker-compose.yaml' file. The default port is 5432, while the user name, password, and dbname are all defined in the 'docker-compose.yaml' file. Please refer to the PostgreSQL service in the 'docker-compose.yaml' file above.

The query
The major difference between SQLite and PostgreSQL is their way of querying. 
(i) SQLite
 query = """INSERT INTO message (name, email, subscribe, message)
               VALUES (:name, :email, :subscribe, :message)"""
(ii) PostgreSQL
 query = """INSERT INTO message (name, email, subscribe, message)
                VALUES (%s, %s, %s, %s)"""

Initilisation of init()
I had imported init() from database.py and executed this initialisation function in app.py. It does not need to run separately.

Host and port
In the previous tutorial, the Flask app's host and port are always set to host='127.0.0.1' and port=5000; therefore, the Flask application will only listen for connections originating from the same computer it is running on. 

However, when I use Docker, it is isolated from my internal network and has its own separate localhost (127.0.0.1), and therefore my browser is unable to communicate with the Flask app. As the chain breaks at the Docker Container boundary, the request gets dropped. Therefore, my browser will indicate an error "Connection Refused."

To avoid the above error, I need to configure host='0.0.0.0' and port=5000. The Flask app inside the container: "Listen for traffic coming from anywhere, including outside this container".  This allows Docker's port forwarding (e.g., -p 5000:5000) to successfully pass traffic from my actual physical machine's browser straight into the Flask app. It is up and running as usual.


Step 3: Dockerise the Flask app, PostgreSQL and PgAdmin
Once all has been set, it is now time to run the Docker container; simply, under the Bash terminal, input the Docker command line as follows:
docker-compose up --build
The following terminal displayed
Now go ahead and open 2 browsers 
(i) Flask app - http://localhost:5000
(ii) PgAdmin - http://localhost:8080


Step 4: Configure PgAdmin and test-run the Flask app
(a) Configure PgAdmin
Once I have logged in to PgAdmin, let me register the server.

Then follow the steps below:
(i) General tab
(ii) Connection tab
(b) Test-run the Flask app and check the record in PgAdmin
Once logged into PgAdmin, go to the side menu under the database, click mydb, then under the top menu "Tools," click the drop-down menu "Query Tool," or on the keyboard, hold the "Alt + Shift + Q" to call out the PgAdmin command box. Next, input this command line: "SELECT * FROM public.message ORDER BY id ASC" and click the "Execute Query" button on the top menu bar, and the table will display the current row of data. 

Then, go ahead to my Flask app, fill out all the necessary fields, including name, email, and message text; select the "subscribe" option; and click the submit button. The Flask app will be redirected to the thank-you page to indicate the submission was successful. 

Now, return to PgAdmin; once again, click the same button, and the table will update with a new row of data that I just submitted.

Final wrap-up:
In this tutorial, we combined Flask, CKEditor, PostgreSQL, PgAdmin, and Docker to create a practical web application that supports rich text content management and database storage. We learned how to connect a Flask application to a PostgreSQL database running in Docker, manage data through PgAdmin, and integrate CKEditor to provide a user-friendly editing experience. This project demonstrates how these technologies work together to build a modern, scalable, and maintainable application.

Published: June 2026
Last Updated: June 2026

About the Author

Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.

Comments

Popular Posts

How to Design a Location Tracking Module for Desktop Business Systems Using Python?

This tutorial will show you how to create an interactive map application that allows users to input geographic coordinates and visualize locations on an interactive map using Python's Tkinter GUI framework enhanced with TTKBootstrap styling and the TkinterMapView widget.  Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the  standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install ttkbootstrap tkintermapview As usual, we need to import the relevant module and set the root values from tkinter import * import ttkbootstrap as tb import tkintermapview from ttkbootstrap.dialogs import Messagebox root = tb.Window(themename='darkly') root.title('Find My Map') root.geometry("1200x1200") I would like to divide it into three sections.  a) Input Panel Layout,  The headers are mainly a label...

How to Create Flask Forms with CKEditor and Flask-WTF?

In this tutorial, we will be integrating Flask-WTF and Flask-CKEditor into your Flask application! You'll learn how to set up the editor, securely handle formatted HTML content, and create a seamless user experience that enhances any project that requires user-generated content. Let's get started! Prerequisite: This tutorial is part of the Flask CKEditor Project Series. 📚 View the Complete Flask CKEditor Series                                                                                                                                                  ➡ Next Part Preliminary Before I begin, it is recommended to activate the v...

How to Store Application Data with SQLite in Python?

  In the previous tutorial, I used TinyDB as our storage solution to keep application data in a simple JSON-based format. While TinyDB is lightweight and easy to use, many web applications require a more structured and scalable database system. In this tutorial, we will switch to SQLite , a powerful relational database that integrates well with Python and web frameworks. By using SQLite, you will learn how to store, manage, and query data in a more structured way for real-world applications. Prerequisite: This tutorial is part of the Flask CKEditor Project Series. 📚 View the Complete Flask CKEditor Series ⬅ Previous Part                                                                                                         ...