Skip to main content
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Josh Siok
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Josh Siok

Replacing nginx With CommandBox In ColdFusion Dev Environment

By
Published in Comments (4)

Many years ago, when I converted the local ColdFusion development site (for this blog) over to Docker, I started off by proxying my CFML container with an nginx container. I did this because I needed both URL rewriting and SSL certificates in order for my blog's "search engine safe (SES) URLs" to work correctly. At the time, I didn't know much about Docker or nginx or servers; but, I came across an article by George "Gavin" Pickin on how to use nginx to generate self-signed certificates and implement URL rewriting. So, that's what I did.

In the years since, the Ortus Solutions team has continued to jam-out projects that support the ColdFusion community. And it seems that everything that I needed nginx for has since been rolled directly into the CommandBox Docker image.

And, as of this morning, I'm no longer using nginx locally! Simplification for the win!

What I Was Doing With nginx

I can't find Gavin's original tutorial, but I can at least show you what I was doing. In my docker-compose.yaml file, I had two services: nginx and cfml. The nginx container had to mount the cfml file system (/cfml/app/wwwroot) in order to determine if a requested URL mapped to a physical file; and, then to rewrite the request when no file was present.

Here's a truncated version of my docker-compose.yaml file — again note that both services are mounting directories out of the ./cfml/app directory:

services:

  #NGINX container, proxies CFML website.
  nginx:
    build:
      context: "./nginx/docker/"
      dockerfile: "Dockerfile"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "./nginx/docker/config:/etc/nginx"
      # Locally, we need to mount the app code so that the URL rewriting will be able to
      # tell if a file exists before attempting to rewrite the request to the path-info.
       - "./cfml/app/wwwroot:/opt/sites/default"
     depends_on:
       cfml:
         condition: service_healthy

  # CFML website.
  cfml:
    build:
      context: "./cfml/docker/"
      dockerfile: "Dockerfile"
    ports:
      - "8080:8080"
    volumes:
      - "./cfml/app:/app"

Inside nginx, my URL rewrite rules were quite simple. When a request to a non-existent file came through, I rewrote the request to point at /index.cfm and appended the original request URL as the new cgi.path_info value:

################### REWRITE: SES RULES #####################
    # Rewrite for URL Mappings
    # If you don't use SES urls you could do something like this
    # location ~ \.(cfm|cfml|cfc)(.*)$ {
    location @rewrite {
        rewrite ^/(.*)? /index.cfm/$1 last;
        rewrite ^ /index.cfm last;
    }

    ################### LOCATION: ROOT #####################
    location / {
        # First attempt to serve request as file or directory, else it sends it to the @rewrite location
        try_files $uri $uri/ @rewrite;
    }

This way, when a request to:

/about/about-ben-nadel.htm

... come through, the nginx container sees that no such file exists and rewrites it to be:

/index.cfm/about/about-ben-nadel.htm

I'm pretty sure I also had to generate a self-signed SSL certificate and load it into the nginx container; but, I couldn't even tell you how I did that. Gavin outlined it in his original post and I just followed his instructions.

What I'm Doing Now With CommandBox

The CommandBox Docker image, with its auto-generated SSL certificate, Undertow Java server, server.json configuration file, and predicate support now fully replaces my nginx container.

My docker-compose.yaml file no longer has an nginx service; and the existing cfml service now maps public ports 80 and 443 to internal ports:

services:

  # CFML website.
  cfml:
    build:
      context: "./cfml/docker/"
      dockerfile: "Dockerfile"
    ports:
      - "80:8080"
      - "443:8443" # SSL certificate provided via server.json
    volumes:
      - "./cfml/app:/app"

The SSL certificate and the URL rewriting is now handled by the server.json file, which is what CommandBox uses to setup the running container. The web.bindings entry sets up the SSL certificate (automagically) and the web.rules entry sets up the URL rewriting:

{
	"name": "bennadel-dev",
	"app": {
		"cfengine": "adobe@2025.0.7"
	},
	"profile": "development",
	"web": {
		"host": "127.0.0.1",
		"bindings": {
			"HTTP": {
				"enable": true,
				"listen": 8080
			},
			"SSL": {
				"enable": true,
				"listen": 8443
			}
		},
		"rules": [
			"not is-file and not is-directory -> rewrite('/index.cfm%U')"
		],
		"blockCFAdmin": false,
		"blockSensitivePaths": true,
		"blockFlashRemoting": true
	},
	"scripts": {
		"onServerInitialInstall": "cfpm install debugger,graphqlclient,image,mail,scheduler"
	}
}

In this case, the %U in the "rules" is the same as %{REQUEST_URL}, which maps to the Requested URL path.

And that's all I needed! My CommandBox image has successfully collapsed my nginx service and my cfml service into a single service. Less complexity, more simplicity, maximum awesome!

Pre-Baking Adobe ColdFusion 2025 Into My CFML Image

As a final note, I'm also pre-baking the ColdFusion engine into my cfml image. Normally, with a CommandBox container, the specified CFML engine version is downloaded on every start. Which makes the startup times somewhat frustrating. Thankfully Ortus provides a way to run the setup script using the FINALIZE_STARTUP flag; and then captures the state into the resultant Docker image.

To be clear, I don't know how to do this — Claude Code is the one that figured it out for me. But this is what my Dockerfile now looks like:

FROM ortussolutions/commandbox:jdk17

# The application webroot within the mounted /app volume. This is baked into the finalized
# startup script during the build, so changing it requires a rebuild (docker compose build).
ENV APP_DIR=/app/wwwroot

# Copy the server.json so that CommandBox can pre-install the Adobe ColdFusion engine
# during the Docker build. This uses the FINALIZE_STARTUP flag to generate an authoritative
# startup script, which means CommandBox doesn't have to re-download the ColdFusion engine
# or re-evaluate the server configuration on every container start.
COPY ./server.json /app/wwwroot/server.json
RUN export FINALIZE_STARTUP=true && \
	$BUILD_DIR/run.sh && \
	unset FINALIZE_STARTUP

As you can see, it's copying the server.json file into the container and then running the setup script. When I build my cfml image, the Adobe ColdFusion 2025 engine is downloaded and saved. And, whenever I start a new container from said image, the ACF engine is already embedded and the startup times are significantly faster.

The trade-off being that anytime I make changes to the server.json file, I now have to rebuild the image since nothing is automatically picked-up by the running container. At least, that's my understanding.

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

Reader Comments

308 Comments

Options like FINALIZE_STARTUP=true are what make CommandBox such an amazing and invaluable tool for our community...so well thought out.

Brad Wood has always been the go-to for support and incredibly responsive. I wonder if AI has meaningfully reduced that support load for him.

16,198 Comments

@Chris,

If anything, the Ortus team has too much documentation. Which makes it perfect for AI. Even when I was asking AI to help me with the server rewrite rules, the number of 404 dead links it came across while scouring the net was big. Having it synthesize lots of random, disparate data sources, that's the sweet spot.

The number of times I've tried to just find a single page that defines what's available in server.json is crazy 🤪 and I feel like I've never found a good definition. But AI can find what I'm looking for.

308 Comments

@Ben Nadel,

There's so much documentation! It's a good thing, but I too have trouble finding the answers I'm after. Glad it's not just me 😂

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
Managed ColdFusion hosting services provided by:
xByte Cloud Logo