Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Ryan Vikander and Kashif Khan and John Hiner
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Ryan Vikander Kashif Khan John Hiner

Experimenting With SQLite JDBC Connections In Lucee CFML

By
Published in , Comments (2)

Although SQLite has been around for almost 25-years, it seems to be having a moment. In the past year or two, I've heard many people discuss the power of embedding SQLite databases within an application. I've never looked at SQLite before; and, I don't think it necessarily makes sense in the context of a ColdFusion web application; but, as a fun exploration, I wanted to see if I could get ColdFusion to connect to a SQLite database.

View this code in my ColdFusion-SQLite project on GitHub.

One of the compelling features of SQLite is the fact that you don't need to have a separate database engine running—such as Microsoft SQL Sever or MySQL. Instead, SQLite databases are represented as physical, server-local .db files. And, all interfacing with these database files is done entirely through database drivers.

In ColdFusion, consuming a database usually means creating a datasource that uses a JDBC (Java Database Connectivity) driver. Some initial searching seemed to indicate that Java has a built-in SQLite driver. However, when I tried to create a JDBC datasource without an additional driver, I received the following ColdFusion error:

lucee.commons.lang.ClassException: cannot load class through its string name, because no definition for the class with the specified name org.sqlite.JDBC could be found.

I ended up downloading the jdbc-sqlite driver by Xerial from Maven Repository. Its only dependency is the SLF4J logging library; and, since Lucee CFML already ships with SLF4J, I only needed the one JAR file.

Normally, when pulling custom JAR files into a Lucee CFML context, I use Lucee's ability to load JAR files via createObject(). In this case, however, the JAR files needed to be made available outside of my application code. As such, I needed to place the sqlite-jdbc-3.45.3.0.jar file in Lucee's lib-equivalent directory. And, to do that, I created a Dockerfile:

FROM lucee/lucee:5.4.4.38

# Setup the SQLite JDBC driver.
COPY ./docker/cfml/lib/sqlite-jdbc-3.45.3.0.jar /opt/lucee/server/lucee-server/bundles

# Note: We didn't need to copy the SLF4J JAR because Lucee already has one.
# COPY ./docker/cfml/lib/slf4j-api-1.7.36.jar /opt/lucee/server/lucee-server/bundles

This Dockerfile creates a container image for Lucee CFML 5.4.4.38 with the SQLite JDBC driver installed. I then created a docker-compose.yaml file to orchestrate a running instance of this CFML server:

version: "2.4"

services:
  cfml:
    build: "."
    ports:
      - "8888:8888"
    volumes:
      - ./app:/var/www
      # Don't put the databases in a publicly accessible directory!
      - ./databases:/var/www-databases

At this point, running docker compose up in the project directory built the Lucee CFML image, created a local network, and spun-up the ColdFusion server at http://localhost:8888. Finally it was time to try connecting to a SQLite database.

According to the Xerial site, the jdbc-sqlite driver works by defining a connection string that points directly to the physical database file on the server. In the following example, sample.db is the SQLite database file:

jdbc:sqlite:sample.db

So, to try this out, I created a per-application datasource in my Application.cfc component. And, since I know that my application code is located inside the Docker container at /var/www, I used the fully-qualified path, /var/www/test.db:

component {

	this.name = "ColdFusionSqlite";
	this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
	this.sessionManagement = false;
	this.setClientCookies = false;

	// A SQLite database connection pointing to `test.db`.
	this.datasources[ "test" ] = {
		class: "org.sqlite.JDBC",
		connectionString: "jdbc:sqlite:/var/www/test.db"
	};

}

Then, I created a test ColdFusion page to see if I could perform a SELECT against this datasource:

<cfquery name="results" datasource="test">
	SELECT
		( 1 ) AS helloWorld
	;
</cfquery>

<cfdump var="#results#" />

And it worked! The CFQuery tag returned a 1-row query. And, when I looked at the local file system, I could see a 0kb file named test.db.

Now, one of the features that people often discuss in the context of SQLite is how lightweight it is. In fact, it's so lightweight (apparently) that you can completely change the way you think about architecting your data. For example, instead of creating a multi-tenant database in which every row has some sort of userID, you can literally create a separate database for every user.

But, if the physical filepath is part of the JDBC connection string, how would that work? I suppose I could use the ColdFusion Administrator component to programmatically add new datasources to the web context. But, that seemed like a lot of work for this simple exploration.

Thankfully, I came across this page in the CommandBox manual by Ortus Solutions. It turns out, in Lucee CFML, you can provide the CFQuery datasource attribute as a struct. Meaning, you can provide the database connection string directly to the CFQuery tag.

Aside: Providing the connection string directly to the CFQuery is probably a bad idea. It almost certainly doesn't play nicely with connection pooling. And, I have no idea if it's thread-safe. It also doesn't seem to be well documented (other than the attribute being defined as any in the CFML documentation). That said, as an exploration, I'm OK with using this. Check the CFML source code if you want to see what this is doing under the hood.

With the ability to provide a per-CFQuery datasource, I can, in theory, provide a unique database file for every single query. Which means, I can play around with creating a per-user database.

To explore this idea, I revamped by Application.cfc to use a per-application path mapping. For security purposes, I wanted to move my database files out of the web-root. But, I didn't want my code to be tightly coupled to the file location. So, I created a mapping for /databases that points to a secure location inside the Docker container, but outside of the /app/www root:

Note: If you look back at my docker-compose.yaml file, you'll see that I'm mounting this path as a Volume. This way, when the database files are created by the JDBC driver, Docker will sync the files back to my host machine. Which allows the databases to survive Docker container rebuilds.

component {

	this.name = "ColdFusionSqlite";
	this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
	this.sessionManagement = false;
	this.setClientCookies = false;

	// In order to decouple the ColdFusion application from the physical location of the
	// SQLite databases, we're going to use a per-application mapping.
	this.mappings = {
		"/databases": "/var/www-databases"
	};

}

As a context for exploration, I'm going to create task lists in which each user receives their own SQLite database with their own unique task table. But, in order to keep track of the users in the system, I also need to create a root / master database that has a user table.

To hide some of the redundant logic around the SQLite JDBC connection details, I created an abstract base component for my Data Access Gateway (DAO) layer. This base component accepts a filename and generates a private variable called, datasource, to be used in the subsequent CFQuery tags.

You'll notice that, internally, this ColdFusion component is using the expandPath() method in order to transform the /databases per-application mapping into the physical filepath on the server:

component
	output = false
	hint = "I provide the abstract base component for a SQLite data access layer."
	{

	/**
	* I initialize the data access object (DAO) with the given database file.
	*/
	package void function init( required string sqliteFilename ) {

		variables.sqliteFilename = arguments.sqliteFilename;
		// NOTE: We're using a per-application mapping to decouple the actual storage
		// location of the database files. That's why we're using expandPath() - so that
		// Lucee resolves to server's root path.
		variables.sqliteFilepath = expandPath( "/databases/#sqliteFilename#" );

		variables.datasource = {
			class: "org.sqlite.JDBC",
			connectionString: "jdbc:sqlite:#sqliteFilepath#"
		};

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I determine if the physical SQLite database file exists.
	*/
	public boolean function databaseExists() {

		return fileExists( sqliteFilepath );

	}


	/**
	* I delete the file representing the current database.
	*/
	public void function dropDatabase() {

		if ( databaseExists() ) {

			fileDelete( sqliteFilepath );

		}

	}

}

With this abstract base component in place, I then created two concrete ColdFusion components to implement all of the database access logic. I created one component for the "master" database (with the list of users) and one component for the "user" database (with the list of tasks).

Each of these components extends the above SQLiteDAO and then calls super.init() in order to provide the filename for the SQLite database. For the master database, I'm just hard-coding the name, master.db. But, for the user data, each user is going to receive their own SQLite database. As such, the user DAO provides a dynamic expression as the database filename: user_#userID#.db.

Since these databases are being created on-the-fly, each DAO has an ensureDatabase() method which creates the necessary database tables. In this exploration, this is relatively simple since each database only has one table. In a production context, I imagine that managing and migrating data must get quite a bit more complicated.

Anyway, let's look at the user management workflow first, which consumes a MasterDAO.cfc ColdFusion component. For the most part, this is just traditional CRUD (Create, Read, Update, Delete) SQL. But, there are some SQLite-specific things that are non-traditional. For example, SQLite only has four data types (INTEGER, REAL, TEXT, BLOB, and NULL, which is a lack of data). And, when inserting a row, the primary key isn't automatically returned as the generatedKey such as it is with other SQL database drivers. Instead, you have to append RETURNING ROWID to the SQL statement.

In the following Lucee CFML code, I'm using tag islands to author the SQL statements in a CFScript context. Take note that each CFQuery tag is including the attribute:

datasource="#datasource#"

This datasource is the private variable created by the abstract base SQLiteDAO.cfc that each of my data access components is extending. This attribute is what allows each CFQuery tag instance to operate using its own SQLite database connection.

component
	extends = "SQLiteDAO"
	hint = "I provide data access methods for the master database (ie, the list of users)."
	{

	/**
	* I initialize the data access object (DAO) for the root experience.
	*/
	public void function init() {

		super.init( "master.db" );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I add a new user with the given name. The auto-generated ID is returned.
	*/
	public numeric function addUser( required string name ) {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#">
			/* DEBUG: masterDAO.addUser(). */
			INSERT INTO user
			(
				name
			) VALUES (
				<cfqueryparam value="#name#" sqltype="varchar" />
			)
			RETURNING
				ROWID
			;
		</cfquery>
		```

		// NOTE: SQLite doesn't return a "generatedKey". However, when a query ends with
		// "RETURNING ROWID", it will return a query with a single column, id, which
		// contains the generated value.
		return val( results.id );

	}

	/**
	* I delete the users that match the given filters.
	*/
	public void function deleteUsersByFilter(
		numeric id
		) {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#">
			/* DEBUG: masterDAO.deleteUsersByFilter(). */
			DELETE FROM
				user
			WHERE
				TRUE

			<cfif arguments.keyExists( "id" )>
				AND
					id = <cfqueryparam value="#id#" sqltype="integer" />
			</cfif>
			;
		</cfquery>
		```

	}

	/**
	* I ensure that the database tables exist.
	*/
	public void function ensureDatabase() {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#">
			/* DEBUG: masterDAO.ensureDatabase(). */
			CREATE TABLE IF NOT EXISTS `user` (
				`id` INTEGER PRIMARY KEY,
				`name` TEXT NOT NULL
			);
		</cfquery>
		```

	}

	/**
	* I get the users that match the given filters.
	*/
	public array function getUsersByFilter(
		numeric id
		) {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#" returnType="array">
			/* DEBUG: masterDAO.getUsersByFilter(). */
			SELECT
				u.id,
				u.name
			FROM
				user u
			WHERE
				TRUE

			<cfif arguments.keyExists( "id" )>
				AND
					u.id = <cfqueryparam value="#id#" sqltype="integer" />
			</cfif>

			ORDER BY
				u.id ASC
			;
		</cfquery>
		```

		return results;

	}

}

With this data access object, I'm able to create a root page in my ColdFusion application that manages the list of users. To keep this application super simple, all of the CFML pages just POST back to themselves in order to change the application state. As such, both the root page and the user-detail page are broken up (logically) into two sections:

  1. The state change logic at the top.

  2. The view rendering logic at the bottom.

I'm doing zero data validation in this demo. The point here isn't to create a robust application, it's to explore SQLite in the context of ColdFusion.

The root page has two functions: it provides a way to add new users at the top; and, it provides a way to delete individual users from the list.

<cfscript>

	param name="form.action" type="string" default="";
	param name="form.userID" type="numeric" default=0;
	param name="form.userName" type="string" default="";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	// In the root page, we're going to use a master database to track the list of users.
	// Then, each user will receive their OWN DATABASE to record tasks.
	masterDAO = new lib.MasterDAO();
	masterDAO.ensureDatabase();

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	if ( form.action.len() ) {

		// CAUTION: I'm not doing any data massaging or validation here - I just want to
		// keep the code really simple so as to focus on the SQLite aspects.
		switch ( form.action ) {
			case "addUser":

				masterDAO.addUser( form.userName );

			break;
			case "deleteUser":

				user = masterDAO
					.getUsersByFilter( id = val( form.userID ) )
					.first()
				;

				userDAO = new lib.UserDAO( user.id );
				userDAO.dropDatabase();
				masterDAO.deleteUsersByFilter( id = user.id );

			break;
		}

		// BEST PRACTICE: Always refresh the page so that a CMD+R doesn't resubmit the
		// form and perform the same action twice.
		location(
			url = "./index.cfm",
			addToken = false
		);

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	users = masterDAO.getUsersByFilter();

</cfscript>
<cfoutput>
	<!doctype html>
	<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<link rel="stylesheet" type="text/css" href="./public/main.css" />
	</head>
	<body>

		<h1>
			Lucee CFML SQLite Exploration
		</h1>

		<form method="post">
			<input
				type="text"
				name="userName"
				placeholder="Name..."
				value=""
				autofocus
				autocomplete="off"
				size="20"
				data-1p-ignore
			/>
			<button type="submit" name="action" value="addUser">
				Add User
			</button>
		</form>

		<h2>
			Users
		</h2>

		<cfloop index="i" value="user" array="#users#">

			<form method="post" class="item">
				<input
					type="hidden"
					name="userID"
					value="#encodeForHtmlAttribute( user.id )#"
				/>
				<a href="./tasks.cfm?userID=#encodeForUrl( user.id )#">
					#encodeForHtml( user.name )#
				</a>
				<button
					type="submit"
					name="action"
					value="deleteUser"
					class="push-right">
					Delete
				</button>
			</form>

		</cfloop>

		<cfif ! users.len()>
			<p>
				<em>No users.</em>
			</p>
		</cfif>

	</body>
	</html>

</cfoutput>

With this code in place, I can start fleshing out my user list:

A list of users being created.

Every time I add a new user to the application, I'm creating a new user record in the master database. But, this doesn't actually create a user-specific SQLite database yet. The user-specific code is handled in the task-management workflow.

But, before we look at the task workflow, let's look at the UserDAO.cfc ColdFusion component. Just like the MasterDAO.cfc, this component also extends the base SQLiteDAO.cfc. It accepts, as a constructor argument the userID; and, it turns around a passes a user-specific SQLite database filename, user_#userID#.db to the super constructor. This allows every instance of this CFC to operate on a different database.

The rest of the code in this DAO consists of straightforward CRUD operations:

component
	extends = "SQLiteDAO"
	hint = "I provide data access methods for a specific user database."
	{

	/**
	* I initialize the data access object (DAO) for the given user.
	*/
	public void function init( required numeric userID ) {

		// Each user gets their own, unique SQLite database.
		super.init( "user_#userID#.db" );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I add a new task with the given name. The auto-generated ID is returned.
	*/
	public numeric function addTask( required string name ) {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#">
			/* DEBUG: userDAO.addTask(). */
			INSERT INTO task
			(
				name,
				isComplete,
				createdAt
			) VALUES (
				<cfqueryparam value="#name#" sqltype="varchar" />,
				FALSE,
				datetime( "now" )
			)
			RETURNING
				ROWID
			;
		</cfquery>
		```

		// NOTE: SQLite doesn't return a "generatedKey". However, when a query ends with
		// "RETURNING ROWID", it will return a query with a single column, id, which
		// contains the generated value.
		return val( results.id );

	}

	/**
	* I delete the tasks that match the given filters.
	*/
	public void function deleteTasksByFilter(
		numeric id,
		boolean isComplete
		) {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#">
			/* DEBUG: userDAO.deleteTasksByFilter(). */
			DELETE FROM
				task
			WHERE
				TRUE

			<cfif arguments.keyExists( "id" )>
				AND
					id = <cfqueryparam value="#id#" sqltype="integer" />
			</cfif>
			;
		</cfquery>
		```

	}

	/**
	* I ensure that the database tables exist.
	*/
	public void function ensureDatabase() {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#">
			/* DEBUG: userDAO.ensureDatabase(). */
			CREATE TABLE IF NOT EXISTS `task` (
				`id` INTEGER PRIMARY KEY,
				`name` TEXT NOT NULL,
				`isComplete` INTEGER NOT NULL CHECK ( isComplete IN ( 0, 1 ) ),
				`createdAt` TEXT NOT NULL
			);
		</cfquery>
		```

	}

	/**
	* I get the tasks that match the given filters.
	*/
	public array function getTasksByFilter(
		numeric id,
		boolean isComplete
		) {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#" returnType="array">
			/* DEBUG: userDAO.getTasksByFilter(). */
			SELECT
				t.id,
				t.name,
				t.isComplete,
				t.createdAt
			FROM
				task t
			WHERE
				TRUE

			<cfif arguments.keyExists( "id" )>
				AND
					t.id = <cfqueryparam value="#id#" sqltype="integer" />
			</cfif>

			<cfif arguments.keyExists( "isComplete" )>
				AND
					t.isComplete = <cfqueryparam value="#isComplete#" sqltype="integer" />
			</cfif>

			ORDER BY
				t.id ASC
			;
		</cfquery>
		```

		return results;

	}

	/**
	* I update the task with the given ID.
	*/
	public void function updateTask(
		required numeric id,
		boolean isComplete
		) {

		```
		<cfquery name="local.results" result="local.metaResults" datasource="#datasource#">
			/* DEBUG: userDAO.updateTask(). */
			UPDATE
				task
			SET
				<cfif arguments.keyExists( "isComplete" )>
					isComplete = <cfqueryparam value="#isComplete#" sqltype="integer" />,
				</cfif>

				id = id
			WHERE
				id = <cfqueryparam value="#id#" sqltype="integer" />
			;
		</cfquery>
		```

	}

}

Since this ColdFusion component creates and caches the datasource variable, I need to create a unique instance of the CFC on each page request. However, doing so will automatically create a SQLite database file on disk. As such, in my task management workflow, it's insufficient to only use the UserDAO.cfc. I have to first check with the MasterDAO.cfc to see if the user exists. And, if so, only then go about instantiating my UserDAO.cfc with the validated user ID:

<cfscript>

	param name="url.userID" type="numeric" default=0;
	param name="form.action" type="string" default="";
	param name="form.taskID" type="string" default=0;
	param name="form.taskName" type="string" default="";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	users = new lib.MasterDAO()
		.getUsersByFilter( id = val( url.userID ) )
	;

	if ( ! users.len() ) {

		location(
			url = "./index.cfm",
			addToken = false
		);

	}

	user = users.first();
	userDAO = new lib.UserDAO( user.id );
	userDAO.ensureDatabase();

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	if ( form.action.len() ) {

		// CAUTION: I'm not doing any data massaging or validation here - I just want to
		// keep the code really simple so as to focus on the SQLite aspects.
		switch ( form.action ) {
			case "addTask":

				userDAO.addTask( form.taskName );

			break;
			case "deleteTask":

				userDAO.deleteTasksByFilter( id = val( form.taskID ) );

			break;
			case "toggleTask":

				task = userDAO
					.getTasksByFilter( id = val( form.taskID ) )
					.first()
				;
				userDAO.updateTask(
					id = task.id,
					isComplete = ! task.isComplete
				);

			break;
		}

		// BEST PRACTICE: Always refresh the page so that a CMD+R doesn't resubmit the
		// form and perform the same action twice.
		location(
			url = "./tasks.cfm?userID=#encodeForUrl( user.id )#",
			addToken = false
		);

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	tasks = userDAO.getTasksByFilter();

</cfscript>
<cfoutput>
	<!doctype html>
	<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<link rel="stylesheet" type="text/css" href="./public/main.css" />
	</head>
	<body>

		<h1>
			#encodeForHtml( user.name )#
		</h1>

		<p>
			&larr; <a href="./index.cfm">Back to users</a>
		</p>

		<form method="post" action="./tasks.cfm?userID=#encodeForUrl( user.id )#">
			<input
				type="text"
				name="taskName"
				placeholder="Task..."
				value=""
				autofocus
				autocomplete="off"
				size="40"
				data-1p-ignore
			/>
			<button type="submit" name="action" value="addTask">
				Add Task
			</button>
		</form>

		<h2>
			Tasks
		</h2>

		<cfloop index="i" value="task" array="#tasks#">

			<form method="post" action="./tasks.cfm?userID=#encodeForUrl( user.id )#" class="item">
				<input
					type="hidden"
					name="taskID"
					value="#encodeForHtmlAttribute( task.id )#"
				/>
				<button
					type="submit"
					name="action"
					value="toggleTask">
					Toggle
				</button>
				<span data-completed="#yesNoFormat( task.isComplete )#">
					#dateFormat( task.createdAt, "yyyy-mm-dd" )#:
					#encodeForHtml( task.name )#
				</span>
				<button
					type="submit"
					name="action"
					value="deleteTask"
					class="push-right">
					Delete
				</button>
			</form>

		</cfloop>

		<cfif ! tasks.len()>
			<p>
				<em>No tasks.</em>
			</p>
		</cfif>

	</body>
	</html>

</cfoutput>

With this CFML code in place, I can now go in and start creating unique tasks for users:

Tasks being created, deleted, and toggles across a variety of users.

Each of these users is being given their own SQLite database, which is auto-generated by the JDBC driver when we make the first SQL query against a given database. And, since I went it an touched all four task lists, I can see all four databases in the local file system:

bennadel@KXYFL7MY7G databases % pwd
/Users/bennadel/Sites/ColdFusion-Sqlite/databases
bennadel@KXYFL7MY7G databases % ls -l
total 80
-rw-r-----  1 bennadel  staff  8192 May 12 07:00 master.db
-rw-r-----  1 bennadel  staff  8192 May 12 07:15 user_1.db
-rw-r-----  1 bennadel  staff  8192 May 12 07:16 user_2.db
-rw-r-----  1 bennadel  staff  8192 May 12 07:15 user_3.db
-rw-r-----  1 bennadel  staff  8192 May 12 07:15 user_4.db

How cool is that?!

I'm not sure how relevant SQLite is for a ColdFusion application; but, it's great to see that it's feasible for a ColdFusion application to create and connect to a SQLite database. Of course, passing on-the-fly datasource values to every CFQuery tag is probably not a scalable solution. Instead, I'd probably want to dynamically add datasources to the Admin; and then use simple datasource names per CFQuery tag. This way, you get proper connection pooling and all the other goodness that's baked into whatever ColdFusion does behind the scenes with datasources.

But, a fun exploration nonetheless!

Want to use code from this post? Check out the license.

Reader Comments

15,848 Comments

After writing this post, I went back and listened to a Changelog podcast on SQLite from 2016. It's a fun interview with the creator of SQLite - he wasn't even a database guy, he was a server admin. And, he created SQLite because the DB they were using at work wasn't always coming online after a server reboot.

15,848 Comments

In my Dockerfile, I was saving the SQLite JAR file into the /bundles folder. I have updated that to use the /lib folder. So, I made this change, from:

/opt/lucee/server/lucee-server/bundles

... to:

/opt/lucee/server/lucee-server/context/lib

This is based on this Lucee Dev forum post, in which they detail the locations of the lib folders inside a Docker based setup:

Re: The info on the Custom Jars doc page,

In the Docker Tomcat version it seems that

{local web context}/WEB-INF/lucee/lib/

translates into

/usr/local/tomcat/lib/

and

{Lucee Installation Folder}/lucee-server/context/lib/

translates into:

/opt/lucee/server/lucee-server/context/lib/

I am not entirely sure what the bundles folder is - I get lost pretty quickly in the low-level server stuff.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel