Skip to main content

Environment variables with special characters in Docker Compose: the dollar sign problem and how to recreate containers

Rogelio Guerra Riverón
Author
Rogelio Guerra Riverón
Building my own web infrastructure from scratch. Here I document each step: servers, networks, containers and everything that comes along.

The Real Problem
#

I’ve been running services on my home server with Docker Compose for months. Recently I tried setting a password with special characters in my .env file. The password was something like Pass$word123!@. When starting the containers, the variable arrived empty or malformed. After investigating, I discovered that Docker Compose was interpreting the $ as a reference to another variable.

Why It Happens: Variable Interpolation
#

Docker Compose interprets the .env file in a special way. When it encounters a $ followed by a valid variable name, it tries to substitute it with its value. If that variable doesn’t exist, it leaves it empty or generates a silent error.

Example of the problem:

# .env
DB_PASSWORD=Pass$word123
API_KEY=sk_test_$random_key
SECRET=$UNDEFINED_VAR

In these cases, Docker Compose will look for variables called word123, random_key, and UNDEFINED_VAR. Obviously it won’t find them, and your values will become corrupted.

The Solution: Single Quotes
#

The most reliable solution is to wrap values in single quotes. Single quotes prevent variable interpolation in Docker Compose, exactly like they work in bash.

# .env - FORMA CORRECTA
DB_PASSWORD='Pass$word123'
API_KEY='sk_test_$random_key'
SECRET='$UNDEFINED_VAR'
COMPLEX='!@#$%^&*()'

With single quotes, Docker Compose treats the entire content as literal text. It doesn’t try to resolve variable references.

Using Variables in docker-compose.yml
#

Once correctly defined in .env, using them in your docker-compose.yml is straightforward:

version: '3.8'
services:
  database:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USER}
  api:
    image: mi-api:latest
    environment:
      API_KEY: ${API_KEY}
      DB_SECRET: ${SECRET}

Docker Compose will load the values from .env and inject the variables correctly into the containers.

The Second Problem: Restart Doesn’t Reload Variables
#

Here comes the frustrating part. After modifying your .env file, you run:

docker-compose restart

And you discover that the containers are still using the old values. This happens because restart only restarts existing containers without recreating them. It doesn’t read the .env file again.

The Solution: –force-recreate
#

For Docker Compose to read the .env file again and apply the new variables, you must recreate the containers. The correct command is:

docker-compose up -d --force-recreate

Or if you prefer a more explicit sequence:

docker-compose down
docker-compose up -d

The --force-recreate option forces recreation even if the image hasn’t changed. Without it, Docker Compose might reuse existing containers.

My Current Workflow
#

After experimenting with this, here’s how I handle variables on my server:

  1. Define everything in .env with single quotes:

    MYSQL_ROOT_PASSWORD='R00t!$pecial#Pass'
    MYSQL_USER='admin'
    MYSQL_PASSWORD='Pass$word@123'
  2. Reference in docker-compose.yml:

    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
  3. After modifying .env, always use:

    docker-compose up -d --force-recreate
  4. Verify that the changes were applied:

    docker-compose exec servicio env | grep MI_VAR

Lessons Learned
#

  • Special characters $, !, @, # in values require single quotes
  • restart only restarts, it doesn’t reload configuration
  • --force-recreate is essential after modifying .env
  • Always verify that variables have been loaded correctly inside the container

These details saved many hours of debugging in my home setup. I hope they save you some too.


Recommended Equipment#

Affiliate links. No extra cost to you.