-
Notifications
You must be signed in to change notification settings - Fork 0
CGI allows you to build lightweight endpoints in Bash (or any language with a shebang), without needing a full application server. It’s useful for tasks like returning dynamic JSON, invoking CLI tools, or integrating with your local filesystem.
Note
CGI is simple and powerful for many tasks, but not efficient for high-frequency workloads.
We'll use caddy-cgi. If you're OK with Caddy handling the response (200 every time), use Exec instead.
Make a directory for scripts:
mkdir caddy/scripts
Build the Caddy image with the caddy-cgi module, copying our scripts into the image:
caddy/Dockerfile
FROM caddy:2-builder AS builder
RUN xcaddy build \
--with github.com/aksdb/caddy-cgi/v2
# Final lightweight image
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
# Copy our scripts into the image
COPY scripts /usr/local/cgi-bin
# Copy our Caddyfile into the image
COPY Caddyfile /etc /caddy/Caddyfile
If you want to add extras (like bash
, jq
, curl
or a scripting language
such as Python), add a line such as:
RUN apk add --no-cache bash jq curl
Build the image:
docker compose build caddy
Mount the scripts
directory in the Compose override file (which affects
development only):
compose.override.yaml
caddy:
volumes:
- ./caddy/scripts:/usr/local/cgi-bin:ro
caddy/Caddyfile
cgi /path /usr/local/cgi-bin/myscript.sh arg1 arg2
Make sure scripts are executable.
chmod +x caddy/scripts/myscript.sh
Lastly, recreate the Caddy container:
docker compose up -d --force-recreate caddy
Output goes to:
-
stdout
goes to the response. -
stderr
goes to Caddy's logs.
The stdout
output must include headers:
caddy/scripts/myscript.sh
#!/bin/sh
echo "Content-Type: text/plain"
echo
echo 'Hello'
cgi
will respond with HTTP status 200 even if the script fails, and the body
will be whatever had been output up to the point of failure.
If you want script failures to respond with 500 Internal Server Error, and the details logged, use this wrapper script:
caddy/scripts/entry.sh
Click to expand
#!/bin/sh
set -euo pipefail
TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT
handle_error() {
code=$?
echo "Error $code in $1 on line $2" >&2
echo "Status: 500 Internal Server Error"
echo "Content-Type: text/plain"
echo
echo "Internal Server Error"
exit 0
}
TARGET=$1
shift
trap 'handle_error "$TARGET" $LINENO' ERR
# --- Source logic script in subshell, redirecting output ---
(
source "$TARGET" "$@"
) >"$TMP"
# --- Only reached on success ---
cat "$TMP"
And update your route:
cgi /my_route /usr/local/bin/entry.sh myscript.sh
The entry.sh
script should be executable, but the scripts it runs don't need
to be.
chmod +x caddy/scripts/entry.sh
If your myscript.sh
is bash, the entry.sh
should also use bash.