I was working on a small project aimed at simplifying the creation and tracking of various types of office documents. The project should include a database of all documents entered, along with their associated due dates. These documents pertain to both clients and suppliers, so the system needs to handle multiple aspects.

My idea was to use Django as the backend framework and deploy the application using Docker. It was a fun experience overall. Initially, my project was structured like this:

businesshub/
   |_ accounts
   |_ anagrafiche
   |_ businesshub
   |_ core
   |_ documents
   |_ templates
.env
.env.prod
docker-compose.yml
Dockerfile
entrypoint.sh
manage.py
requirements.txt

My Dockerfile was as follows:

# Use a base image with Python
FROM python:3.11-slim

# Environment variables to prevent bytecode generation and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Set the working directory inside the container
WORKDIR /code

# Install system dependencies needed for the project
RUN apt-get update && \
    apt-get install -y python3 python3-pip python3-dev netcat-openbsd wget build-essential \
    libffi-dev libpango1.0-0 libpangocairo-1.0-0 libcairo2 libjpeg-dev \
    zlib1g-dev libxml2 libxslt1.1 libgdk-pixbuf2.0-0 unzip

# Upgrade pip and install the Python dependencies from requirements.txt
RUN pip3 install --upgrade pip
COPY requirements.txt .
RUN pip3 install -r requirements.txt

# Copy the project files into the container
COPY . .

# Set executable permissions for the entrypoint script
RUN chmod +x /code/entrypoint.sh

# Command to run when the container starts
ENTRYPOINT ["/bin/sh", "/code/entrypoint.sh"]

my docker-compose.yml was like this

services:
  web:
    build: .
    command: /code/entrypoint.sh
    volumes:
      - .:/code
      - static_volume:/code/staticfiles
    ports:
      - '8000:8000'
    env_file: .env
    depends_on:
      - db
    environment:
      - DB_HOST=db

  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - '5434:5432'
  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
    volumes:
      - static_volume:/code/staticfiles
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:

And my entrypoint.sh was like this

#!/bin/sh

# Set the Django settings module
export DJANGO_SETTINGS_MODULE=businesshub.settings

# Wait for the database to be ready
echo "⏳ Waiting for the database at $DB_HOST..."
retries=10
while ! nc -z $DB_HOST 5432; do
  retries=$((retries-1))
  if [ $retries -eq 0 ]; then
    echo "❌ Timeout: Unable to connect to the database!"
    exit 1
  fi
  sleep 1
done
echo "✅ Database is available!"

set -e

# Create migrations if there are any changes to models
echo "🔄 Creating migrations..."
python3 manage.py makemigrations

# Apply database migrations
echo "🔄 Applying database migrations..."
python3 manage.py migrate

# Check if Django is running in DEBUG mode
DEBUG_MODE=$(python3 -c "from django.conf import settings; print(settings.DEBUG)")

# If in production, collect static files and start the Gunicorn server
if [ "$DEBUG_MODE" = "False" ]; then
  echo "📦 Collecting static files (production only)..."
  python3 manage.py collectstatic --noinput

  echo "🚀 Starting Gunicorn server..."
  exec gunicorn businesshub.wsgi:application \
    --bind 0.0.0.0:8000 \
    --workers 3 \
    --timeout 120
else
  # If in DEBUG mode, start the Django development server
  echo "⚙️  DEBUG mode: starting Django development server..."
  exec python3 manage.py runserver 0.0.0.0:8000
fi

Yes, I use some AI help to comment these files.

At this point, I had the idea to integrate Jasper Reports in order to create some reports. Usually, you can use something like ReportLabs, but I wanted to create reports with Jasper Studio, put them into my container, and call them with some Django view.

So, I started to search for some documentation to understand how to do this. The best way seemed to be using Jasper Server and interacting with it through API calls. However, I found that Jasper Server is no longer available in a free version, which made things a bit tricky. The commercial version requires a license, and while there are some alternatives, I couldn’t find a free solution that would integrate well with my current setup.

Instead of using Jasper Server, I ended up integrating JasperReports directly into my Docker container, using a custom Java helper to generate reports. This way, I could still leverage the powerful report generation features of Jasper without relying on an external server, which simplified the architecture and saved some overhead. In order to do this, I used Django subprocess.
First of all you need to download Jasper Report libraries. So I change my Dockerfile in this way:

FROM openjdk:11-jdk-slim

# Environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Set the working directory inside the container
WORKDIR /code

# Install system dependencies needed for the project
RUN apt-get update && \
    apt-get install -y python3 python3-pip python3-dev netcat-openbsd wget build-essential \
    libffi-dev libpango1.0-0 libpangocairo-1.0-0 libcairo2 libjpeg-dev \
    zlib1g-dev libxml2 libxslt1.1 libgdk-pixbuf2.0-0 unzip

# Upgrade pip and install the Python dependencies from requirements.txt
RUN pip3 install --upgrade pip
COPY requirements.txt .
RUN pip3 install -r requirements.txt

# Copy the project files into the container
COPY . .

# Copy jreports folder with .jasper e .jrxml files
COPY jreports /code/jreports

# Create the folder for JasperReports JAR 
RUN mkdir -p /opt/jasperreports/lib

# Download JasperReports 7.0.2 and dependencies
RUN wget https://repo1.maven.org/maven2/net/sf/jasperreports/jasperreports/7.0.2/jasperreports-7.0.2.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/org/apache/commons/commons-collections4/4.4/commons-collections4-4.4.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/org/jfree/jfreechart/1.5.3/jfreechart-1.5.3.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/org/jfree/jcommon/1.0.23/jcommon-1.0.23.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/kernel/7.1.16/kernel-7.1.16.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/io/7.1.16/io-7.1.16.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/layout/7.1.16/layout-7.1.16.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/forms/7.1.16/forms-7.1.16.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/pdfa/7.1.16/pdfa-7.1.16.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/sign/7.1.16/sign-7.1.16.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/barcodes/7.1.16/barcodes-7.1.16.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/org/eclipse/jdt/ecj/3.21.0/ecj-3.21.0.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/commons-digester/commons-digester/2.1/commons-digester-2.1.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/commons-beanutils/commons-beanutils/1.9.4/commons-beanutils-1.9.4.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/commons-logging/commons-logging/1.2/commons-logging-1.2.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.2.2/commons-collections-3.2.2.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/org/slf4j/slf4j-simple/1.7.30/slf4j-simple-1.7.30.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/net/sf/jasperreports/jasperreports-pdf/7.0.2/jasperreports-pdf-7.0.2.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/itextpdf/commons/7.2.0/commons-7.2.0.jar -P /opt/jasperreports/lib/ && \
    wget https://repo1.maven.org/maven2/com/github/librepdf/openpdf/1.3.30/openpdf-1.3.30.jar -P /opt/jasperreports/lib/


# Copy ReportGenerator.java 
COPY ReportGenerator.java /opt/jasperreports/

# Compile ReportGenerator.java file
RUN cd /opt/jasperreports && \
    javac -cp "lib/*" ReportGenerator.java && \
    mkdir -p classes && \
    mv ReportGenerator.class classes/

RUN wget https://jdbc.postgresql.org/download/postgresql-42.5.0.jar -O /opt/jasperreports/lib/postgresql-42.5.0.jar

RUN chmod +x /code/entrypoint.sh

# Comando di avvio del container
ENTRYPOINT ["/bin/sh", "/code/entrypoint.sh"]

I had to change the base image from
FROM python:3.11-slim
to
FROM openjdk:11-jdk-slim
because JasperReports relies on Java to function. The original Python image doesn't include the necessary Java runtime environment, which is required by JasperReports to generate reports.

JasperReports is a Java-based reporting tool, so it needs the Java Development Kit (JDK) to run properly. The Python image, while great for running Python applications, doesn't come with Java installed, and without it, JasperReports wouldn't be able to work. By switching to an image that already includes OpenJDK, we ensure that we have the necessary Java environment to run the JasperReports library and its dependencies.
Then, I created a folder called jreports where I stored my .jrxml and .jasper files that I created with Jasper Studio. Using

COPY jreports /code/jreports

I copied them into the container folder. Next, I created a directory to store the required dependencies with this command:

RUN mkdir -p /opt/jasperreports/lib

Now comes the hardest part: finding and downloading the correct dependencies. This was the most challenging task. It wasn't easy to find the right versions, and I spent a lot of time searching through forums and documentation for solutions. I spent quite some time troubleshooting with the help of ChatGPT and Claude. They kept suggesting dependencies and versions that blocked the Docker build because the files were nonexistent or the paths were incorrect. So with a lot of patience, finally I came to a list that works.
The next step was to integrate my ReportGenerator.java file into the container. ReportGenerator.java is the one that makes possible to use Jasper in Django. This is the file that I call when I need to build the report.

ReportGenerator.java
import net.sf.jasperreports.engine.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;

public class ReportGenerator {
    public static void main(String[] a# Imposta i permessi di esecuzione per entrypoint.shrgs) {
        try {
            if (args.length < 6) {
                System.err.println(
                        "Usage: java ReportGenerator      [param1=value1 param2=value2 ...]");
                System.exit(1);
            }

            String templatePath = args[0];
            String outputPath = args[1];
            String dbUrl = args[2];
            String dbUser = args[3];
            String dbPassword = args[4];

            System.out.println("Template path: " + templatePath);
            System.out.println("Output path: " + outputPath);
            System.out.println("Database URL: " + dbUrl);

            Map parameters = new HashMap<>();
            for (int i = 5; i < args.length; i++) {
                String[] param = args[i].split("=", 2);
                if (param.length == 2) {
                    if (param[0].equals("PK")) {
                        try {
                            long pkValue = Long.parseLong(param[1]);
                            parameters.put(param[0], pkValue);
                            System.out.println("Parameter: " + param[0] + " = " + pkValue + " (Long)");
                        } catch (NumberFormatException e) {
                            System.err.println("Error: PK parameter must be a number");
                            System.exit(1);
                        }
                    } else {
                        parameters.put(param[0], param[1]);
                        System.out.println("Parameter: " + param[0] + " = " + param[1]);
                    }
                }
            }

            Class.forName("org.postgresql.Driver");
            System.out.println("PostgreSQL JDBC driver loaded.");

            Connection connection = DriverManager.getConnection(dbUrl, dbUser, dbPassword);
            System.out.println("Database connection established.");

            Statement stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery(
                    "SELECT id, numero_interno, plafond FROM documenti_dichiarazioneintento WHERE id = "
                            + parameters.get("PK"));
            if (rs.next()) {
                System.out.println("Record found: ID=" + rs.getLong("id") +
                        ", numero_interno=" + rs.getInt("numero_interno") +
                        ", plafond=" + rs.getBigDecimal("plafond"));
            } else {
                System.out.println("No records found with PK=" + parameters.get("PK"));
            }
            rs.close();
            stmt.close();

            System.out.println("Filling report with parameters: " + parameters);
            JasperPrint jasperPrint = JasperFillManager.fillReport(templatePath, parameters, connection);

            System.out.println("Report compiled successfully.");
            System.out.println("Number of pages: " + jasperPrint.getPages().size());

            JasperExportManager.exportReportToPdfFile(jasperPrint, outputPath);
            System.out.println("Report generated successfully: " + outputPath);

            connection.close();
            System.exit(0);
        } catch (Exception e) {
            System.err.println("Error generating report: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }
}

The ReportGenerator.java class plays a critical role in generating the JasperReports PDF report in this setup. It acts as a bridge between your Django application and the JasperReports engine. Let’s break down its functionality and how it integrates with the Django project.

Key Components of ReportGenerator.java:
Dependencies and Libraries: The class uses various Java libraries, most notably the JasperReports library itself, as well as JDBC for database connectivity. These libraries allow the class to:

Fill the report with data fetched from the database.

Export the report to a PDF file.

Handle parameters passed by the Django application (e.g., the primary key PK of the record).

Here’s how the dependencies are integrated:

JasperReports Engine (net.sf.jasperreports.engine): This is the core library for compiling, filling, and exporting reports.

PostgreSQL JDBC Driver (org.postgresql.Driver): Used for establishing a connection with the PostgreSQL database to fetch the data needed for the report.

Main Method: The main method is where the process of generating the report begins. This method accepts several arguments:

templatePath: The path to the Jasper template file (.jasper).

outputPath: The path where the generated PDF report will be saved.

dbUrl, dbUser, dbPassword: Credentials and URL for connecting to the PostgreSQL database.

Additional parameters: These can be dynamic parameters that the report template expects, such as a specific PK (primary key) value to fetch the correct data.

Parameter Handling:

The class accepts dynamic parameters as command-line arguments (e.g., PK=12345).

These parameters are parsed and added to a Map, which is passed to the JasperReports engine when filling the report. This allows you to customize the report’s content based on the provided values.

For example:

The PK parameter is particularly important. It represents the primary key of a specific record, and based on this key, the corresponding record is fetched from the database to populate the report.

Database Connection:

The class connects to the PostgreSQL database using the provided JDBC URL and credentials.

A Statement is created to execute an SQL query to fetch the necessary data for the report. In this case, the SQL query looks for a specific record in the documenti_dichiarazioneintento table by matching the PK value.

Example query:

SELECT id, numero_interno, plafond FROM documenti_dichiarazioneintento WHERE id = ?

If the record exists, it’s used to populate the report parameters; if not, an error message is displayed.

Filling the Report:

Once the data is retrieved from the database, the JasperFillManager.fillReport() method is called. This method takes the compiled .jasper file, the parameters, and the database connection to generate a filled report (JasperPrint object).

The JasperPrint object contains all the content of the report, including text, images, and dynamic data from the database.

Exporting the Report:

After the report is filled with the required data, it is exported to a PDF file using the JasperExportManager.exportReportToPdfFile() method.

The generated PDF is then saved to the outputPath location, which can be returned to the user through the Django view.

Error Handling:

The class includes basic error handling. If there’s an issue generating the report (e.g., if the database connection fails or if the report template is invalid), the error is caught, and an error message is printed.

Integration with Django:
Calling the Java Class: From Django, you invoke the ReportGenerator Java class by using the subprocess.run() method. This allows you to run Java commands as if they were shell commands, passing the required parameters like the template path, output path, database URL, and credentials.

Example:

command = [
    "java",
    "-cp",
    "/opt/jasperreports/classes:/opt/jasperreports/lib/*",
    "ReportGenerator",
    report_path,
    output_path,
    db_url,
    db_user,
    db_password,
    f"PK={pk}",  # Pass PK parameter
]

The command is constructed with the appropriate classpath (-cp), which includes the necessary JAR files for JasperReports and PostgreSQL JDBC. Then the Java class (ReportGenerator) is invoked with the parameters passed in the correct order.

Parameters:

When the Django view is called (in this case, dichiarazione_intento), it passes the PK value for the document to be included in the report.

This PK value is used in the SQL query inside ReportGenerator.java to fetch the correct record from the PostgreSQL database.

Subprocess Execution:

The Java process is executed in the background, and once it finishes, the generated PDF is saved to a file.

The view then reads the generated PDF and returns it to the user as a response.

Conclusion:
The ReportGenerator.java class is essential for integrating JasperReports with Django. It acts as the backend logic for compiling, filling, and exporting the report based on dynamic parameters and database queries. This integration leverages Java's powerful reporting capabilities while allowing Django to trigger the report generation process via subprocess calls.

By separating the report generation into a standalone Java class, you maintain flexibility and modularity in the design. JasperReports remains a powerful and customizable tool for generating complex reports, and this solution allows you to use it seamlessly within a Django application.

So I copy this in my container with

COPY ReportGenerator.java /opt/jasperreports/

and compile it

RUN cd /opt/jasperreports && \
    javac -cp "lib/*" ReportGenerator.java && \
    mkdir -p classes && \
    mv ReportGenerator.class classes/

Then I download postgresql java dependencies

RUN wget https://jdbc.postgresql.org/download/postgresql-42.5.0.jar -O /opt/jasperreports/lib/postgresql-42.5.0.jar

But, maybe I can put this wget with others? I will try.

Finally I start my entrypoint.sh file.

So, at this point my project will be like this

businesshub/
   |_ accounts
   |_ anagrafiche
   |_ businesshub
   |_ core
   |_ documents
   |_ jreports
      |_DichIntento.jasper
      |_DichIntento.jrxml
   |_ nginx
      |_default.conf
   |_ templates
.env
.env.prod
docker-compose.yml
Dockerfile
entrypoint.sh
manage.py
ReportGenerator.java
requirements.txt

And the view to call my report is

import subprocess
from django.http import HttpResponse
from django.conf import settings
import os
from documenti.models import DichiarazioneIntento


def dichiarazione_intento(request, pk):
    # Paths
    report_path = "/code/jreports/DichIntento.jasper"
    output_path = "/code/jreports/output_report.pdf"

    # Get database credentials from environment variables
    db_host = os.environ.get("DB_HOST", "localhost")
    db_port = os.environ.get("DB_PORT", "5432")
    db_user = os.environ.get("POSTGRES_USER", "postgres")
    db_password = os.environ.get("POSTGRES_PASSWORD", "")
    db_name = os.environ.get("POSTGRES_DB", "postgres")

    # Create JDBC URL
    db_url = f"jdbc:postgresql://{db_host}:{db_port}/{db_name}"

    # Get the object to verify it exists
    dichiarazione = DichiarazioneIntento.objects.get(pk=pk)
    print(f"dichiarazione: {dichiarazione.data_dichiarazione}")
    print(f"Generating report with PK: {pk}")

    # Build Java command
    command = [
        "java",
        "-cp",
        "/opt/jasperreports/classes:/opt/jasperreports/lib/*",
        "ReportGenerator",
        report_path,
        output_path,
        db_url,
        db_user,
        db_password,
        f"PK={pk}",  # Pass PK parameter
    ]

    print(f"Executing command: {' '.join(command)}")

    # Execute Java command
    try:
        result = subprocess.run(
            command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        print(f"Output: {result.stdout.decode()}")
    except subprocess.CalledProcessError as e:
        error_message = e.stderr.decode() if e.stderr else str(e)
        print(f"Error: {error_message}")
        return HttpResponse(f"Error generating report: {error_message}", status=500)

    # Verify file exists
    if not os.path.exists(output_path):
        return HttpResponse("Generated PDF file not found", status=500)


    with open(output_path, "rb") as pdf_file:
        response = HttpResponse(pdf_file.read(), content_type="application/pdf")
        response["Content-Disposition"] = 'inline; filename="report.pdf"'
        return response

Now that everything is set up, I can start using JasperReports in my Django project. This experience taught me a lot about integrating Java-based tools into a Python/Django environment, and I'm excited to explore more reporting features in the future.

Thank you for reading through this entire guide! I hope you found it helpful and that it provides valuable insights into integrating JasperReports with Django in a Dockerized environment. If you have any questions, suggestions, or tips to improve this process, feel free to leave a comment below—I’d love to hear from you!

If you're interested in learning more about Django, Docker, or JasperReports, make sure to follow me for future updates and more detailed tutorials. Happy coding!