CI/CD for CFML Applications
What is CI/CD?
CI (Continuous Integration) means every code change is automatically tested before it is merged. CD (Continuous Delivery) means every passing build is automatically packaged and ready to deploy.
For a ColdFusion application the full pipeline looks like this:

Tests gate the build — a failing TestBox run stops the Docker image from being created.
📌 Scope of this lesson
Running a complete CI/CD pipeline requires a Git server, a container registry, and a CI runner. That infrastructure is out of scope for this foundations course.
In this lesson you will build the two artefacts the pipeline depends on: a box.json manifest and a Dockerfile. Then you will build a Docker image locally — the same command the pipeline runs.
The full pipeline — Git server, registry, automated tests, and deployments using Gitea — is covered in the ColdFusion Advanced course.
1. box.json — the project manifest
box.json is the CommandBox project manifest. It declares your app name, version, and ForgeBox dependencies — similar to package.json in Node.js or composer.json in PHP.
{
"name": "helpdesk-app",
"version": "1.0.0",
"dependencies": {
"testbox": "^5.0.0"
}
}
box install reads this file and installs all declared packages into a modules/ directory. In a CI pipeline the build agent runs box install first — this file is the only thing it needs to recreate the full dependency tree on a clean machine.
2. Dockerfile — containerise your app

The base image handles the runtime — you just copy code and install packages.
FROM ortussolutions/commandbox:latest
COPY . /app
WORKDIR /app
RUN box install --production
EXPOSE 8888
CMD ["box", "server", "start", "--console"]
| Line | What it does |
|---|---|
FROM ortussolutions/commandbox:latest | Official CommandBox base — Java + Lucee already bundled |
COPY . /app | Copies your CFML project files into the image |
RUN box install --production | Installs ForgeBox dependencies, skips dev packages like TestBox |
EXPOSE 8888 | Documents which port the server listens on |
CMD [...] | Starts the Lucee server in the foreground when the container runs |
💡 Why not bake credentials into the Dockerfile?
A Docker image is a portable artefact — anyone who can pull it can inspect every layer. Credentials baked into the image (datasource passwords, API keys) are exposed to anyone with registry access, and they end up in git history too.
The right pattern is to inject credentials at runtime via environment variables. In a CI pipeline, secrets are stored in the CI server (GitHub Actions Secrets, Gitea Secrets) and passed to the container on start — never written into the image.
Activity 1 — Create box.json
Create /home/laborant/app/box.json:
mkdir -p /home/laborant/app
tee /home/laborant/app/box.json << 'EOF'
{
"name": "helpdesk-app",
"version": "1.0.0",
"dependencies": {
"testbox": "^5.0.0"
}
}
EOF
Verify:
cat /home/laborant/app/box.json
Activity 2 — Create the Dockerfile
Create /home/laborant/app/Dockerfile:
tee /home/laborant/app/Dockerfile << 'EOF'
FROM ortussolutions/commandbox:latest
COPY . /app
WORKDIR /app
RUN box install --production
EXPOSE 8888
CMD ["box", "server", "start", "--console"]
EOF
Verify:
cat /home/laborant/app/Dockerfile
Activity 3 — Build the Docker image
Build the image tagged cfml-app:
docker build -t cfml-app /home/laborant/app/
Confirm it exists:
docker images | grep cfml
🐢 First build is slow — here is why.
Docker pulls the ortussolutions/commandbox:latest base image on the first build — roughly 500 MB. Subsequent builds reuse cached layers and complete in seconds. The RUN box install --production step is also cached after the first run as long as box.json has not changed.
When all the checks above are green, this lesson is complete. Your progress is saved automatically — move straight on to the next lesson.
Found a bug or an issue with this lesson? Please reach out — your feedback helps improve the course for everyone.
📧 Alex — mercadoalexatgmail.com
- Previous lesson
- Performance Tuning & JVM Configuration
- Next lesson
- Production Readiness & Monitoring