Stefano Ferrari
Posted on 10 de abr.
Django, PostgreSQL, Docker and Jasper
Reports.
#jasperreports #django #docker #postgres
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
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-essent
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:
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
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-essent
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/jasper
wget https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.12.0/commons
wget https://repo1.maven.org/maven2/org/apache/commons/commons-collections4/4.4/com
wget https://repo1.maven.org/maven2/org/jfree/jfreechart/1.5.3/jfreechart-1.5.3.jar
wget https://repo1.maven.org/maven2/org/jfree/jcommon/1.0.23/jcommon-1.0.23.jar -P
wget https://repo1.maven.org/maven2/com/itextpdf/kernel/7.1.16/kernel-7.1.16.jar -P
wget https://repo1.maven.org/maven2/com/itextpdf/io/7.1.16/io-7.1.16.jar -P /opt/ja
wget https://repo1.maven.org/maven2/com/itextpdf/layout/7.1.16/layout-7.1.16.jar -P
wget https://repo1.maven.org/maven2/com/itextpdf/forms/7.1.16/forms-7.1.16.jar -P /
wget https://repo1.maven.org/maven2/com/itextpdf/pdfa/7.1.16/pdfa-7.1.16.jar -P /op
wget https://repo1.maven.org/maven2/com/itextpdf/sign/7.1.16/sign-7.1.16.jar -P /op
wget https://repo1.maven.org/maven2/com/itextpdf/barcodes/7.1.16/barcodes-7.1.16.ja
wget https://repo1.maven.org/maven2/org/eclipse/jdt/ecj/3.21.0/ecj-3.21.0.jar -P /o
wget https://repo1.maven.org/maven2/commons-digester/commons-digester/2.1/commons-d
wget https://repo1.maven.org/maven2/commons-beanutils/commons-beanutils/1.9.4/commo
wget https://repo1.maven.org/maven2/commons-logging/commons-logging/1.2/commons-log
wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.2.2/c
wget https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar
wget https://repo1.maven.org/maven2/org/slf4j/slf4j-simple/1.7.30/slf4j-simple-1.7.
wget https://repo1.maven.org/maven2/net/sf/jasperreports/jasperreports-pdf/7.0.2/ja
wget https://repo1.maven.org/maven2/com/itextpdf/commons/7.2.0/commons-7.2.0.jar -P
wget https://repo1.maven.org/maven2/com/github/librepdf/openpdf/1.3.30/openpdf-1.3.
# 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/jasperrepor
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,
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
try {
if (args.length < 6) {
System.err.println(
"Usage: java ReportGenerator <template_path> <output_path> <db_
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<String, Object> 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] + " = " + pkVal
} 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, dbPasswo
System.out.println("Database connection established.");
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT id, numero_interno, plafond FROM documenti_dichiarazioneint
+ 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, parame
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.
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
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/jasperrepor
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
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
# 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!
Top comments (0)
Code of Conduct Report abuse
ACI.dev PROMOTED
ACI.dev: Fully Open-source AI Agent Tool-Use Infra
(Composio Alternative)
100% open-source tool-use platform (backend, dev portal, integration library,
SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth,
granular permissions, and access through direct function calling or a unified
MCP server.
Check out our GitHub!
Stefano Ferrari
Interested in Python, Django, javascript and web-dev.
LOCATION
Italy
WORK
Owner of E-Solutions
JOINED
10 de mai. de 2020
More from Stefano Ferrari
Add toast notification with Django - an easy way
javascript beginners django bootstrap
Django Environment Variables
django python webdev
django python
Django: timedelta, DurationField and Total Time
python django programming
Sentry PROMOTED
Make it make sense
Only get the information you need to fix your code that’s broken with Sentry.
Start debugging →