Creating a LuCLI + Nginx Stack for Lucee CFML
curl -L -o lucli.jar https://github.com/cybersonic/LuCLI/releases/latest/download/lucli.jar
docker compose up --build -d
open http://localhost:8080
View at github.com/andyj/lucli-docker-nginx-starter
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:
- lucee: Builds from
docker/lucli/Dockerfile, copies inlucli.jar, and runsjava -jar lucli.jar server run --port 8080 --webroot /var/www. - nginx: Uses Alpine Nginx, mounts the same
www/directory, and proxies CFML requests tolucee:8080while 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.