Back to the notebook

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.

Leave a note.