How to Fix Docker Unexpected Operator Error: A Complete Troubleshooting Guide

How to Fix Docker Unexpected Operator Error: A Complete Troubleshooting Guide

If you’ve landed here, you’ve probably seen something like this in your build logs:

/bin/sh: 1: [: !=: unexpected operator

or perhaps:

/bin/sh: 1: [: ==: unexpected operator

That cryptic message has ruined many CI pipelines and frustrated many a developer (myself included, on more than one bleary-eyed Tuesday night). The good news is that this is one of the most predictable Docker errors you’ll ever encounter — once you understand why it happens, you’ll spot it from a mile away.

This guide walks through how to fix docker unexpected operator error in all its common (and a few uncommon) forms, with copy-paste-ready fixes, root-cause analysis, and prevention tips that will save you from repeating the same mistake.


What Does “Unexpected Operator” Actually Mean?

Before we jump into fixes, let’s decode what the shell is trying to tell us.

The error originates from test (also written as [), the POSIX shell built-in that evaluates conditions. When test encounters a token it doesn’t recognize as a valid operator — like == instead of =, or when arguments are missing — it bails out with “unexpected operator.”

Inside a Docker container, this almost always surfaces from:

  • A RUN instruction in your Dockerfile
  • An ENTRYPOINT or CMD shell script
  • A shell script that runs during build or startup

The reason it’s so common in Docker is subtle: most Docker base images run /bin/sh, which is often dash, not bash. And dash is strict about POSIX compliance. Constructs that work fine in your local bash shell silently break inside the container.


Root Cause Analysis

Let’s break down the most frequent culprits.

1. Using == Inside [ ] (POSIX test)

In bash, both = and == work for string comparison inside [ ]. In dash (the default /bin/sh on Debian/Ubuntu), only = is valid.

Faulty Dockerfile snippet:

RUN if [ "$NODE_ENV" == "production" ]; then \
      npm prune --production; \
    fi

On node:20-bookworm (Debian 12-based), /bin/sh is symlinked to dash, which chokes on ==.

2. Missing Quotes Around Variables

When $VAR is empty or unset, the comparison collapses:

if [ $ENV == "prod" ]; then ...

If ENV is empty, the shell sees:

if [ == "prod" ]; then ...

…and you get the unexpected operator error.

3. Using [[ ]] When the Script Runs Under sh

[[ ]] is a bash/ksh extension. Under /bin/sh, it’s a syntax error or — depending on the shell — produces operator errors.

RUN if [[ "$DEBUG" == "true" ]]; then echo "debug on"; fi

4. Wrong Shebang in Entrypoint Scripts

#!/bin/sh
if [ "$1" == "start" ]; then ...

The shebang says sh, but the script uses bashisms. This is one of the sneakiest causes because it works perfectly during local testing (where /bin/sh might be bash) and then fails in the container.

5. Incorrect Use of Arithmetic Operators for Strings (or Vice Versa)

if [ "$PORT" -eq "8080" ]; then ...

-eq is for integers. If PORT happens to contain non-numeric characters, you’ll get a different — but related — error. Conversely, using = for numbers works but is semantically misleading and can break numeric comparison.


Step-by-Step Solutions: Most Common to Edge Cases

Solution 1: Replace == with = (Most Common Fix)

This single change resolves the majority of “unexpected operator” errors in Dockerfiles.

Before:

FROM node:20-bookworm-slim
ARG NODE_ENV=production
RUN if [ "$NODE_ENV" == "production" ]; then \
      npm prune --production; \
    fi

After:

FROM node:20-bookworm-slim
ARG NODE_ENV=production
RUN if [ "$NODE_ENV" = "production" ]; then \
      npm prune --production; \
    fi

That’s it. One character.

Pro tip: When porting shell snippets into Dockerfiles, run them through dash -n script.sh locally to catch POSIX issues early. On macOS, install dash via Homebrew: brew install dash.

Solution 2: Always Quote Your Variables

Even if you fix ===, unquoted variables will still bite you when they’re empty or contain spaces.

Robust pattern:

if [ "${NODE_ENV:-}" = "production" ]; then
  npm prune --production
fi

The ${VAR:-default} syntax provides an empty default, preventing the “argument expected” variant of the error.

Solution 3: Explicitly Invoke bash for Complex Logic

If your script uses [[ ]], arrays, or other bash-only features, don’t fight POSIX. Just use bash explicitly.

Option A — In the Dockerfile:

FROM python:3.12-slim
SHELL ["/bin/bash", "-c"]

RUN if [[ "$DEBUG" == "true" ]]; then \
      pip install debugpy; \
    fi

The SHELL instruction changes the default shell for subsequent RUN, CMD, and ENTRYPOINT instructions.

Option B — For an entrypoint script:

FROM python:3.12-slim
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]

Make sure bash is actually installed in the image. On slim images, you may need:

RUN apt-get update && apt-get install -y --no-install-recommends bash \
    && rm -rf /var/lib/apt/lists/*

Solution 4: Fix the Shebang in Entrypoint Scripts

If you author a script that uses bash features, declare bash in the shebang:

#!/usr/bin/env bash
set -euo pipefail

if [[ "${1:-}" == "migrate" ]]; then
  python manage.py migrate
fi

exec "$@"

Common mistake: #!/bin/sh at the top, but [[ ]] or == inside. Either change the shebang to #!/bin/bash or rewrite the logic in POSIX.

A useful sanity check: run shellcheck on your script. It catches bashisms and operator misuse. Install via apt install shellcheck or brew install shellcheck.

Solution 5: Validate Arithmetic Comparisons

If you’re comparing numbers, use arithmetic operators (-eq, -lt, -gt) and ensure variables are numeric:

if [ "${REPLICAS:-0}" -eq 0 ]; then
  echo "No replicas configured"
  exit 1
fi

Or, cleaner, use arithmetic expansion:

if (( REPLICAS == 0 )); then
  echo "No replicas configured"
fi

Note that (( )) requires bash — see Solution 3.

Solution 6: Watch Out for Variable Substitution in CMD/ENTRYPOINT

The shell form of CMD runs under /bin/sh -c, which is dash on Debian images. If you inline a comparison there:

# Problematic
CMD if [ "$APP_MODE" == "worker" ]; then celery worker; else gunicorn app:wsgi; fi

Fix:

CMD if [ "$APP_MODE" = "worker" ]; then celery worker; else gunicorn app:wsgi; fi

Or move the logic into a script and copy it in.

Solution 7: Edge Case — Locale and Character Encoding Issues

Rare, but I’ve seen this in production: an environment variable contains an invisible character (e.g., a carriage return from a Windows-edited .env file), which makes the comparison fail.

Symptoms: the error message includes odd characters or the comparison “should match” but doesn’t.

Fix:

# Strip CR characters
NODE_ENV=$(echo "$NODE_ENV" | tr -d '\r')
if [ "$NODE_ENV" = "production" ]; then ...

Better yet, ensure your .env and shell scripts use Unix line endings. In VS Code, check the bottom-right of the editor for CRLF and switch to LF.

Solution 8: Edge Case — Alpine’s ash Quirks

Alpine uses BusyBox ash as /bin/sh. It’s mostly POSIX but has a few quirks. For example, local works in functions (a non-POSIX extension), but some parameter expansions behave differently.

If you see “unexpected operator” on Alpine and your Dockerfile looks correct, test the exact command inside an Alpine container:

docker run --rm -it alpine:3.19 sh
/ # [ "a" = "a" ] && echo ok
ok
/ # [ "a" == "a" ] && echo ok
sh: ==: unknown operand

Note that Alpine’s error message differs slightly (“unknown operand” instead of “unexpected operator”) — same root cause.


A Real-World Example I Hit Last Month

I was building a multi-stage Dockerfile for a Django service. The image built fine on my Mac but failed in CI with:

=> ERROR [stage-1 7/7] RUN if [ "$DJANGO_SETTINGS_MODULE" == "config.settings.prod" ]; then python manage.py collectstatic --noinput; fi    0.4s
------
> [stage-1 7/7] RUN if [ "$DJANGO_SETTINGS_MODULE" == "config.settings.prod" ]; then python manage.py collectstatic --noinput; fi:
#12 0.374 /bin/sh: 1: [: ==: unexpected operator

The fix was, of course, changing == to =. But the real lesson was that I should have been using shellcheck in CI. After adding it to the pipeline, I caught three more bashisms before they ever reached Docker.


Prevention Tips: How to Never See This Error Again

1. Run shellcheck in CI

Add this to your GitHub Actions workflow:

name: Lint shell scripts
on: [push, pull_request]
jobs:
  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ludeeus/action-shellcheck@master
        with:
          severity: warning

It will flag == inside [ ], unquoted variables, and bashisms in #!/bin/sh scripts.

2. Standardize on POSIX sh in Dockerfiles

If you don’t need bash features, stick to POSIX. It’s more portable and works across Debian, Alpine, and distroless images. The key rules:

  • Use = not ==
  • Use [ ] not [[ ]]
  • Use $(command) not backticks
  • Quote every variable expansion
  • Use { VAR:-default } for safe defaults

3. Add set -eu (or set -euo pipefail with bash)

#!/bin/sh
set -eu

if [ "${DEBUG:-}" = "1" ]; then
  echo "Debug mode enabled"
fi
  • -e exits on any error
  • -u treats unset variables as errors (catches the “empty variable” variant of this bug)

4. Pin and Test Base Images Locally

Don’t assume /bin/sh behaves the same everywhere. Test your build inside the actual target image:

docker run --rm -it python:3.12-slim sh

Then run your script line by line.

5. Use the SHELL Instruction Deliberately

If you want bash semantics throughout your Dockerfile:

FROM ubuntu:24.04
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
RUN ...

This ensures every RUN uses bash with strict error handling. Just remember that bash must be installed in the image.


Debugging Workflow: A Quick Checklist

When the error appears, walk through this:

  1. Identify the failing line — Docker prints the exact RUN instruction.
  2. Find any [ ] or [[ ]] constructs — these are the prime suspects.
  3. Check for == — replace with =.
  4. Check for unquoted variables — quote everything.
  5. Check the shebang if it’s a script — match it to the syntax you’re using.
  6. Check the base image’s /bin/shls -l /bin/sh inside the container.
  7. Run shellcheck on the offending snippet.
  8. Test locally in the same image with docker run --rm -it <image> sh.

Key Takeaways

  • The “unexpected operator” error in Docker almost always comes from a test ([) command receiving an operator the current shell doesn’t support.
  • The single most common cause is using == inside [ ] when /bin/sh is dash (Debian/Ubuntu default) or ash (Alpine default).
  • Replace == with = to fix 80% of cases immediately.
  • Always quote variables: "$VAR" or "${VAR:-}" for safety.
  • If you need bash features ([[ ]], arrays), invoke bash explicitly via the SHELL instruction or change your script’s shebang.
  • Run shellcheck in CI to catch these issues before they hit Docker.
  • Use set -eu in shell scripts to fail fast on unset variables and errors.

Master these patterns and this error will essentially disappear from your workflow.


FAQ

Q: I fixed == to = but still get the error. What now?

Check that your variable is actually set. Add set -u to surface unset variables, or use the safe expansion form ${VAR:-}. Also verify you’re editing the right file — Docker uses build cache aggressively, and a stale layer can mask your fix. Run with --no-cache to be sure.

Q: Does this error happen on Alpine too?

Yes, though the message differs slightly. Alpine uses BusyBox ash, which reports “unknown operand” for the same == issue. The fix is identical: use = inside [ ].

Q: Why does my script work locally but fail in Docker?

On macOS and many Linux distros, /bin/sh is symlinked to bash, which accepts == inside [ ]. Inside most Docker base images, /bin/sh is dash (Debian/Ubuntu) or ash (Alpine), both of which are strict POSIX and reject ==. Use readlink /bin/sh locally to confirm.

Q: Should I just always use SHELL ["/bin/bash", "-c"]?

It depends. For complex build logic, yes — bash is more predictable and featureful. For production images where size matters, sticking to POSIX sh keeps things lean and portable. If you don’t need bash features, there’s no reason to add it as a dependency.

Q: Can I use [[ ]] in a Dockerfile RUN instruction?

Not by default. The RUN instruction uses /bin/sh -c unless you override it with the SHELL instruction. If you want [[ ]], add SHELL ["/bin/bash", "-c"] near the top of your Dockerfile and ensure bash is installed in the base image.


If this guide helped you resolve the issue, the next step is prevention: wire shellcheck into your CI pipeline today. It takes five minutes and eliminates this entire class of errors going forward. Happy building.

Leave a Reply

Your email address will not be published. Required fields are marked *