Python

Koala’s Minimal python image built on 0-deb through distroless design significantly reduces CVEs in the final image. The python image is production-ready image optimized for python workloads.

Koala’s python image is available at multiple registries, including dockerhub regsitry & koala’s public ecr .

Koala’s Python demo flask app

Use the following command to download the koala python image locally:

docker pull koalalab/python

This guide helps in setting a sample flask demo for building a full application on top of koala’s minimal python image.

Start by making a Koala Demo folder

mkdir Koala-Demo
cd Koala-Demo

Save the following file as demo.yml

services:
  web:
    image: public.ecr.aws/b7m2q1o2/flask-demo
    ports:
      - "0.0.0.0:5001:5001"
    environment:
      - DB_HOST=db
    depends_on:
      - db
    restart: unless-stopped


  db:
    image: postgres:17
    environment:
      - POSTGRES_DB=flask_app
      - POSTGRES_USER=flask_user
      - POSTGRES_PASSWORD=your_actual_password
    ports:
      - "5432:5432"
    volumes:
      - pg_data:/var/lib/postgresql/data
    restart: unless-stopped


volumes:
  pg_data:

In your terminal, run the following commands

brew install docker-compose
docker-compose -f demo.yml up -d

After this you should be able to access flask demo app at http://localhost:5001

& would look something like this:

Flask demo app built on koala's minimal python image



Editing & running the flask demo

The above was a sample demo created by KoalaLab.

If you want to build the image locally & run it yourself, here are the steps:

  1. Create a local dockerfile, save this as flask.dockerfile

ARG VENV_NAME=koala

# Multi-arch build using koalalab/python images
FROM koalalab/python:latest-builder AS builder

ARG VENV_NAME

RUN python3 -m venv $VENV_NAME

ENV PATH="$WORKDIR/$VENV_NAME/bin:$PATH"

COPY flask/requirements.txt flask/requirements.txt

RUN pip install --no-cache-dir -r flask/requirements.txt

COPY flask flask

FROM koalalab/python

ARG VENV_NAME

COPY --from=builder $WORKDIR $WORKDIR

ENV PATH="$WORKDIR/$VENV_NAME/bin:$PATH"

# Expose Flask port
EXPOSE 5001

CMD ["python3", "flask/app.py"]

2. Create a sub-folder called flask

mkdir flask
cd flask

3. Create two files in the flask folder

  • app.py

  • requirements.txt

4. Edit the python file app.py include the following code

from flask import Flask, request, jsonify, render_template_string
import psycopg2
from psycopg2.extras import RealDictCursor
import os
from datetime import datetime
import logging

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = Flask(__name__)

# Database configuration
DATABASE_CONFIG = {
    'host': os.getenv('DB_HOST', 'localhost'),
    'database': os.getenv('DB_NAME', 'flask_app'),
    'user': os.getenv('DB_USER', 'flask_user'),
    'password': os.getenv('PASSWORD', 'your_actual_password'),
    'port': os.getenv('DB_PORT', '5432')
}

def get_db_connection():
    """Create and return a database connection"""
    try:
        conn = psycopg2.connect(**DATABASE_CONFIG)
        return conn
    except psycopg2.Error as e:
        logger.error(f"Database connection error: {e}")
        return None

def init_database():
    """Initialize the database with required tables"""
    conn = get_db_connection()
    if not conn:
        logger.error("Could not connect to database for initialization")
        return False

    try:
        with conn.cursor() as cursor:
            # Create users table
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS users (
                    id SERIAL PRIMARY KEY,
                    name VARCHAR(100) NOT NULL,
                    email VARCHAR(100) UNIQUE NOT NULL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)
            conn.commit()
            logger.info("Database initialized successfully")
            return True
    except psycopg2.Error as e:
        logger.error(f"Database initialization error: {e}")
        conn.rollback()
        return False
    finally:
        conn.close()

# HTML template for the web interface
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
    <title>Flask PostgreSQL App</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .form-group { margin: 10px 0; }
        input, button { padding: 8px; margin: 5px; }
        button { background: #007bff; color: white; border: none; cursor: pointer; }
        button:hover { background: #0056b3; }
        .user { background: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 5px; }
        .error { color: red; }
        .success { color: green; }
    </style>
</head>
<body>
    <h1>Flask PostgreSQL User Management</h1>

    <div id="message"></div>

    <h2>Add New User</h2>
    <form id="userForm">
        <div class="form-group">
            <input type="text" id="name" placeholder="Name" required>
        </div>
        <div class="form-group">
            <input type="email" id="email" placeholder="Email" required>
        </div>
        <button type="submit">Add User</button>
    </form>

    <h2>Users</h2>
    <div id="users"></div>
    <button onclick="loadUsers()">Refresh Users</button>

    <script>
        function showMessage(text, isError = false) {
            const messageDiv = document.getElementById('message');
            messageDiv.textContent = text;
            messageDiv.className = isError ? 'error' : 'success';
            setTimeout(() => messageDiv.textContent = '', 3000);
        }

        document.getElementById('userForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const name = document.getElementById('name').value;
            const email = document.getElementById('email').value;

            try {
                const response = await fetch('/api/users', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ name, email })
                });

                const data = await response.json();
                if (response.ok) {
                    showMessage('User added successfully!');
                    document.getElementById('userForm').reset();
                    loadUsers();
                } else {
                    showMessage(data.error || 'Error adding user', true);
                }
            } catch (error) {
                showMessage('Network error', true);
            }
        });

        async function loadUsers() {
            try {
                const response = await fetch('/api/users');
                const users = await response.json();

                const usersDiv = document.getElementById('users');
                if (users.length === 0) {
                    usersDiv.innerHTML = '<p>No users found</p>';
                } else {
                    usersDiv.innerHTML = users.map(user =>
                        `<div class="user">
                            <strong>${user.name}</strong> - ${user.email}
                            <br><small>Created: ${new Date(user.created_at).toLocaleString()}</small>
                            <button onclick="deleteUser(${user.id})" style="float: right; background: #dc3545;">Delete</button>
                        </div>`
                    ).join('');
                }
            } catch (error) {
                showMessage('Error loading users', true);
            }
        }

        async function deleteUser(userId) {
            if (!confirm('Are you sure you want to delete this user?')) return;

            try {
                const response = await fetch(`/api/users/${userId}`, { method: 'DELETE' });
                const data = await response.json();

                if (response.ok) {
                    showMessage('User deleted successfully!');
                    loadUsers();
                } else {
                    showMessage(data.error || 'Error deleting user', true);
                }
            } catch (error) {
                showMessage('Network error', true);
            }
        }

        // Load users on page load
        loadUsers();
    </script>
</body>
</html>
"""

@app.route('/')
def index():
    """Serve the main page"""
    return render_template_string(HTML_TEMPLATE)

@app.route('/health')
def health_check():
    """Health check endpoint"""
    conn = get_db_connection()
    if conn:
        conn.close()
        return jsonify({'status': 'healthy', 'database': 'connected'})
    else:
        return jsonify({'status': 'unhealthy', 'database': 'disconnected'}), 500

@app.route('/api/users', methods=['GET'])
def get_users():
    """Get all users"""
    conn = get_db_connection()
    if not conn:
        return jsonify({'error': 'Database connection failed'}), 500

    try:
        with conn.cursor(cursor_factory=RealDictCursor) as cursor:
            cursor.execute("SELECT * FROM users ORDER BY created_at DESC")
            users = cursor.fetchall()
            return jsonify([dict(user) for user in users])
    except psycopg2.Error as e:
        logger.error(f"Error fetching users: {e}")
        return jsonify({'error': 'Failed to fetch users'}), 500
    finally:
        conn.close()

@app.route('/api/users', methods=['POST'])
def create_user():
    """Create a new user"""
    data = request.get_json()

    if not data or not data.get('name') or not data.get('email'):
        return jsonify({'error': 'Name and email are required'}), 400

    conn = get_db_connection()
    if not conn:
        return jsonify({'error': 'Database connection failed'}), 500

    try:
        with conn.cursor(cursor_factory=RealDictCursor) as cursor:
            cursor.execute(
                "INSERT INTO users (name, email) VALUES (%s, %s) RETURNING *",
                (data['name'], data['email'])
            )
            user = cursor.fetchone()
            conn.commit()
            return jsonify(dict(user)), 201
    except psycopg2.IntegrityError:
        conn.rollback()
        return jsonify({'error': 'Email already exists'}), 409
    except psycopg2.Error as e:
        logger.error(f"Error creating user: {e}")
        conn.rollback()
        return jsonify({'error': 'Failed to create user'}), 500
    finally:
        conn.close()

@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    """Get a specific user"""
    conn = get_db_connection()
    if not conn:
        return jsonify({'error': 'Database connection failed'}), 500

    try:
        with conn.cursor(cursor_factory=RealDictCursor) as cursor:
            cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
            user = cursor.fetchone()

            if not user:
                return jsonify({'error': 'User not found'}), 404

            return jsonify(dict(user))
    except psycopg2.Error as e:
        logger.error(f"Error fetching user: {e}")
        return jsonify({'error': 'Failed to fetch user'}), 500
    finally:
        conn.close()

@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    """Delete a user"""
    conn = get_db_connection()
    if not conn:
        return jsonify({'error': 'Database connection failed'}), 500

    try:
        with conn.cursor() as cursor:
            cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))

            if cursor.rowcount == 0:
                return jsonify({'error': 'User not found'}), 404

            conn.commit()
            return jsonify({'message': 'User deleted successfully'})
    except psycopg2.Error as e:
        logger.error(f"Error deleting user: {e}")
        conn.rollback()
        return jsonify({'error': 'Failed to delete user'}), 500
    finally:
        conn.close()

@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not found'}), 404

@app.errorhandler(500)
def internal_error(error):
    return jsonify({'error': 'Internal server error'}), 500

if __name__ == '__main__':
    # Initialize database on startup
    if init_database():
        logger.info("Starting Flask application...")
        app.run(debug=True, host='0.0.0.0', port=5001)
    else:
        logger.error("Failed to initialize database. Exiting.")
        exit(1)

5. Edit the requirements.txt file to save the following code

Flask==3.0.0
psycopg2-binary==2.9.10
python-dotenv==1.0.0

6. Come back to previous folder

cd ..

7. Edit line 3 of demo.yml to the following line

Remove this

image: public.ecr.aws/b7m2q1o2/flask-demo

to

Change to:

image: local-flask-demo

8. Run the following command in terminal

docker build -t local-flask-demo -f flask.dockerfile .

9. Run the demo through this terminal command

docker-compose -f demo.yml up -d

After this you should be able to access the local flask demo app at http://localhost:5001