Skip to content

CGI ‐ Common Gateway Interface

Beau Barker edited this page Jul 29, 2025 · 1 revision

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.

Caddy

Make a directory for scripts:

mkdir caddy/cgi

Build the Caddy image with additions:

  • Add the caddy-cgi module.
  • Copy 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 ./cgi /cgi

# Copy our Caddyfile into the image
COPY Caddyfile /etc /caddy/Caddyfile

You may want to add bash and jq or some other scripting language such as Python. Add a line such as:

RUN apk add --no-cache bash jq

Build the image:

docker compose build caddy

Mount the Scripts

Mount the cgi scripts directory in the Compose override file (which affects development only):

compose.override.yaml

caddy:
  volumes:
    - ./caddy/cgi:/cgi:ro

Add a Route

caddy/Caddyfile

cgi /my_route myscript.sh

Make sure scripts are executable.

chmod +x caddy/cgi/myscript.sh

Restart Caddy

Lastly, recreate the Caddy container:

docker compose up -d --force-recreate caddy

Writing Scripts

Output goes to:

  • stdout goes to the response.
  • stderr goes to Caddy's logs.

The stdout output must include headers:

caddy/cgi/myscript.sh

#!/bin/sh

echo "Content-Type: text/plain"
echo
echo 'Hello'

Shell Script Error Handling

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 failures to respond correctly (500 Internal Server Error), and the details logged, use this wrapper script:

caddy/cgi/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
}
trap 'handle_error "$TARGET" $LINENO' ERR

TARGET=$1
shift

# --- Source logic script in subshell, redirecting output ---
(
  source "$TARGET" "$@"
) >"$TMP"

# --- Only reached on success ---
cat "$TMP"

And update your route:

cgi /my_route entry.sh myscript.sh {
  dir /cgi
}

Note

The entry.sh must be executable, but the scripts it runs (myscript.sh) do not.

Important

If your script is bash, the entry.sh should be changed to bash.

Clone this wiki locally