Andy Jarrett // Code. Develop. Create.

Creating a LuCLI + Nginx Stack for Lucee CFML

Laptop running LuCLI

Creating a LuCLI + Nginx Stack for Lucee CFML

I spent the last couple of evenings turning a simple Lucee CFML template into a Dockerised sandbox powered by LuCLI. The goal was to run lucli.jar inside a container that behaves like the CLI, hide it behind Nginx, and expose a tiny CFML demo site with scope dumps.

Why LuCLI?

LuCLI (pronounced “Lucee-EL-EYE”) gives me a terminal friendly way to start Lucee servers, run CFML snippets, and manage local dev environments. Instead of relying on a prebuilt Lucee image, I wanted Docker to build on top of the exact CLI JAR so I can track its behaviour and swap versions easily.

Step 1 – Wire up Docker Compose

I created two services in docker-compose.yml:

  1. lucee: Builds from docker/lucli/Dockerfile, copies in lucli.jar, and runs java -jar lucli.jar server run --port 8080 --webroot /var/www.
  2. nginx: Uses Alpine Nginx, mounts the same www/ directory, and proxies CFML requests to lucee:8080 while serving static files directly.

The compose file exposes port 8080 on the host, so http://localhost:8080 shows whatever the Lucee instance renders.

Step 2 – Handle the JAR

Because GitHub releases sometimes block direct downloads during Docker builds, the Dockerfile now copies the locally downloaded lucli.jar by default. If I need a different version, I can run docker compose build --build-arg LUCLI_JAR_URL=https://… lucee and it will fetch the JAR during build.

Step 3 – Add a CFML Playground

Inside www/ I added:

  • Application.cfc – Minimal bootstrap, tracks when the app started.
  • index.cfm – A landing page with navigation that loads the other templates into an iframe.
  • hello.cfm – Classic “Hello, World!” rendered by Lucee.
  • cgi.cfm & server.cfm – Each page dumps the respective scope so I can confirm requests are flowing through Nginx correctly.

The iframe trick keeps the navigation anchored while I poke at each scope. Setting its height with calc(100vh - 200px) makes the dumps readable without scrolling the whole page.

Step 4 – Document Everything

To make the setup reproducible, I refreshed README.md with:

  • Direct download instructions for lucli.jar.
  • Notes on running java -jar lucli.jar server run --port 8080 --webroot ./www.
  • Docker usage docs, including how to rebuild with a different JAR via LUCLI_JAR_URL.

I also updated docs/lucli.md so future me knows the Docker image relies on the local JAR unless overridden.

Bonus – Cleaning Up the Repo

To keep git tidy, .gitignore now excludes the bulky artefacts: lucli.jar, the CLI’s .lucli_home/, temporary extraction directories, and www/WEB-INF/. I removed the previously tracked WEB-INF content so Lucee can recreate it on the fly without polluting commits.

Testing the Stack

Once the files were in place, the workflow boiled down to:

curl -L -o lucli.jar https://github.com/cybersonic/LuCLI/releases/latest/download/lucli.jar
docker compose up --build -d
docker compose logs -f

Hitting http://localhost:8080 showed the iframe-driven index page, and each link rendered inside the frame exactly as expected. When I saw a 502, it was because my Nginx upstream was still pointing at an old internal port. Once I aligned the upstream with the port LuCLI was actually listening on, everything behaved.

What’s Next?

I now have a reproducible LuCLI sandbox for experimenting with CFML routing, URL rewriting, and future Lucee versions. From here I can add TLS termination, automated tests, or even ship this as a starter repo for other CFML devs who want to kick the tyres on LuCLI.

I’m here, learning and working away. If you liked this content and want to keep me going, consider buying me a coffee. Your support keeps this site running and the coffee brewing! ☕️