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
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.
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. # 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"]Flask
Flask-WTF
Flask-CKEditor
psycopg2-binary
WTForms
email-validator
gunicornIt 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.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)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. 🚀 Continue Learning Flask & Python Automation
Get more Flask, Python automation, Docker, and HTMX tutorials delivered to your inbox.
✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates
Download my FREE Flask Cheat Sheet (PDF)
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)"""
docker-compose up --buildNow, return to PgAdmin; once again, click the same button, and the table will update with a new row of data that I just submitted.
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