DevOps

Fix Docker Compose environment 'unexpected type map' error

See the three wordings this error has had across Compose releases, fix the line in either valid form, and avoid the quoted look-alike that hides the error but loses the variable.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

Under environment:, the line - REDIS_HOST: redis is a list item that holds a one-key YAML mapping, not a string. Compose accepts either a list of KEY=value strings or a mapping of KEY: value pairs, so write - REDIS_HOST=redis, or remove the dash and write REDIS_HOST: redis. Current Compose reports the mistake as services.<service>.environment.[0]: unexpected type map[string]interface {}, and the old Python Compose v1 called it an invalid type that "should be a string". The value itself was right all along: redis, the service name, is the host the app should use.

One mistake, three wordings#

The question on Stack Overflow is a Node.js service that needs to reach Redis, with its variable written like this:

yaml
    environment:
      - REDIS_HOST: redis

The asker suspected that line, but had seen it in tutorials:

Clearly the issue is where I set my environment variable even though i've seen multiple tutorials that use the same syntax.

The asker's file was reproduced unchanged on 5 October 2026, on Docker Desktop with Docker Engine 29.7.2, against four Compose releases. All four reject it, and only the wording changes. Docker Compose v5.5.0, and v2.29.1 run from the docker:24-cli image, print:

text
services.login-service.environment.[0]: unexpected type map[string]interface {}

Docker Compose v2.22.0, run from the docker:23-cli image with the file mounted at /w, printed:

text
validating /w/docker-compose.yml: services.login-service.environment.0 must be a string

The Python Compose v1, tested as docker-compose 1.29.2, prints the wording quoted in the question, with whatever value the line holds:

text
The Compose file './docker-compose.yml' is invalid because:
services.login-service.environment contains {"REDIS_HOST": "redis"}, which is an invalid type, it should be a string

Compose v1 is no longer a supported CLI: Docker's history of Compose says "the supported Docker Compose CLI versions are Compose v2 and Compose v5". The number in the path ([0] or .0) is the position of the bad item in the list, counted from zero. The third answer on the thread hit the mistake on its second item and got services.web.environment.1 must be a string. A file built the same way gave exactly that line on v2.22.0, and services.web.environment.[1]: unexpected type map[string]interface {} on v5.5.0.

Why a dash followed by a colon builds a mapping#

In YAML, a leading - starts a list item, and KEY: value (a colon followed by a space) is a key/value pair. Together, - REDIS_HOST: redis is a list whose first item is a mapping with one key. Compose v1 shows this literally: {"REDIS_HOST": "redis"} in its message is that mapping printed as JSON, because its validator formats the offending value with json.dumps.

What Compose expects is written in its schema. The environment attribute refers to a list_or_dict definition: either a mapping whose values are strings, numbers, booleans or null, or an array of unique strings (compose-spec.json in compose-go). Docker's services reference says the same in one line: "environment can use either an array or a map." A list item that is itself a mapping fits neither shape.

Current Compose stops before the schema check, which is why its message reads like a Go type error. Compose v5.5.0 is built on compose-go v2.14.0 (its go.mod), whose loader removes duplicate list entries before it validates the file against the schema. For environment, build.args, labels and several other lists, that step reads each item as a string and splits it at = to find the name. Anything that is not a string falls into the unexpected type %T branch. In Go, %T prints "a Go-syntax representation of the type of the value" (fmt documentation), so map[string]interface {} is the item exactly as the YAML decoder returned it: a map.

Fix#

1. Keep the list and write each item as one KEY=value string#

yaml
    environment:
      - REDIS_HOST=redis

This is the smallest change to the question's file. In the reproduction below, it was the only line that differed between the failing run and the working one.

2. Or drop the dash and use the mapping form#

yaml
    environment:
      REDIS_HOST: redis

Both forms give the same result: docker compose config prints either one as the same mapping, and the runtime check below passed with each. Pick one form per block. A block that mixes - REDIS_HOST=redis with a REDIS_PORT: 6379 line is not valid YAML at all, and Compose v5.5.0 stops on a parser error (did not find expected '-' indicator) before any of its own checks run.

Verify the fix#

docker compose config loads, validates and prints the resolved file. With either fix the error is gone, and the variable appears under the service as a mapping entry:

text
    environment:
      REDIS_HOST: redis

docker compose config --quiet validates without printing the file and exits 0, which suits a CI step.

To check the variable at runtime, the reproduction used a project with the same shape as the question's: a Node service with the project bind-mounted at /usr/src/app, Redis on a user-defined network, and no published ports. The Node service runs a short script, saved as ping-redis.js next to the Compose file, that sends PING to the host named in REDIS_HOST:

js
const net = require('node:net');
 
const host = process.env.REDIS_HOST;
console.log(`REDIS_HOST=${host}`);
 
const socket = net.connect({ host, port: 6379 }, () => socket.write('PING\r\n'));
socket.on('data', (data) => {
  console.log(`Redis replied ${data.toString().trim()}`);
  socket.end();
});
socket.on('error', (err) => {
  const detail = err.errors ? err.errors.map((e) => e.message).join('; ') : err.message;
  console.error(`${err.code}: ${detail}`);
  process.exitCode = 1;
});
yaml
services:
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 3s
      retries: 10
    networks:
      - my-network
 
  login-service:
    image: node:22-alpine
    working_dir: /usr/src/app
    volumes:
      - ./:/usr/src/app
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - my-network
    environment:
      - REDIS_HOST=redis
    command: ["node", "ping-redis.js"]
 
networks:
  my-network:
bash
docker compose run --rm login-service
docker compose down -v

With - REDIS_HOST: redis in that file, the run stopped at the same unexpected type error and no container started. With - REDIS_HOST=redis, and again with the mapping form, it printed:

text
REDIS_HOST=redis
Redis replied +PONG

docker compose run starts Redis first and leaves it running after the one-off container exits, which is why down -v follows. The healthcheck plus condition: service_healthy does the job of the question's wait-for-it.sh. The services reference explains why the plain list is not enough: "With short syntax, Compose does not wait for dependency services to be "healthy" before starting a dependent service." In the run, Compose reported Redis as Healthy before it created the Node container.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

REDIS_HOST takes the service name, not an IP address#

The asker wanted the variable to hold Redis's address:

The purpose of it is to set REDIS_HOST to whatever ip address docker assigns to Redis when building the image.

No address is needed. The Compose networking guide says "Each service registers its name with an internal DNS server, so containers can reach each other using the service name directly." It also warns that container addresses change when containers are recreated: "This means you should always reference services by name, not IP address." The service's key in the file, redis, is therefore the correct value, and the run above reached Redis at that name on port 6379.

The ports: - 6379:6379 line on the Redis service plays no part in that. The test project published no ports at all, and the same guide says of a published port that "The host port is only used when accessing the service from outside the network." Keep the line only if a tool on your machine, outside Docker, must reach Redis. If another process already listens on 6379 on the host, this is the line that cannot publish its port: the Postgres bind: address already in use fix explains that failure with port 5432, how to find what owns the busy host port, and how to publish on another one.

Edits that look like fixes but are not#

Each of these was run on Compose v5.5.0 in the same reproduction.

Quoting the value. - REDIS_HOST: "redis" fails with the same unexpected type map[string]interface {} message. The quotes apply to the value, and the item is still a mapping.

Quoting the whole item. - "REDIS_HOST: redis" makes the error disappear, because the item is now a string. It contains no =, though, so Compose takes the whole text as the name of a variable with no value. docker compose config shows it:

text
'REDIS_HOST: redis': null

The services reference describes what becomes of a name without a value: "If the value is not resolved, the variable is unset and is removed from the service container environment." In the test project, the Node service received no REDIS_HOST at all and fell back to localhost, because Node's net.connect uses 'localhost' when no host is given (Node.js net documentation). The script prints the error code and each address Node tried:

text
REDIS_HOST=undefined
ECONNREFUSED: connect ECONNREFUSED ::1:6379; connect ECONNREFUSED 127.0.0.1:6379

A colon without a space. The highest-voted answer on the thread shows a colon form written USER:john. Without the space, YAML does not see a key/value pair: the YAML specification notes that "“a:1” is a plain scalar and not a key/value pair". The block is then no mapping at all, and Compose v5.5.0 rejects it with services.webserver.environment must be a mapping. Compose v1, run on the same file as 1.29.2, said it differently:

text
services.webserver.environment contains an invalid type, it should be an object, or an array

Written as USER: john, with the space, the same block loads.

A value from the shell. The third answer hit the error with - REDIS_DATABASE_PASSWORD: ${REDIS_DATABASE_PASSWORD} and blamed the file format version. The interpolation is not the cause: the item is a mapping like the others. - REDIS_DATABASE_PASSWORD=${REDIS_DATABASE_PASSWORD} loads, and with the variable set in the shell, docker compose config showed the substituted value.

Deleting the version line. The question's file without version: '3' fails with the identical message. Current Compose ignores that attribute ("Unlike v1, Compose v2 ignores the version top-level element", per the history page), and only once the file is valid does v5.5.0 print a warning that the attribute is obsolete and will be ignored. Remove the line if you like, but it is not part of this fix.

The same item under another key. The step that rejects the item covers more than environment. - NODE_ENV: production under build.args gives services.web.build.args.[0]: unexpected type map[string]interface {}, and a dash-and-colon label gives the same message under services.web.labels.[0]. The cure is the same: - NODE_ENV=production under build.args and - com.example.team=payments under labels both loaded.

Mapping form: quote values that look like numbers#

Choosing the mapping form brings one trap the list form does not have. In a mapping, YAML reads each value first and Compose turns the result into a string; in the list, - NODE_VERSION=20.10 is one string and stays as typed. With NODE_VERSION: 20.10, ZIP_CODE: 0123, DEBUG: True and VERBOSE: yes left unquoted, docker compose config printed:

text
DEBUG: "true"
NODE_VERSION: "20.1"
VERBOSE: 'yes'
ZIP_CODE: "83"

20.10 became the number 20.1, and 0123 became 83, which is 0123 read as an octal number, matching the leading-0 octal form listed on yaml.org's integer type page. The same values quoted in the mapping, or written in the list form, came through unchanged. The services reference already advises quoting booleans: "Any boolean values; true, false, yes, no, should be enclosed in quotes to ensure they are not converted to True or False by the YAML parser." On v5.5.0, yes survived as text while True became true; quoting settles both.

When the error turns into connection refused#

Once the variable reaches the container, the app still has to use it. If the Redis host in the code is still localhost, hard-coded or left at a default, the client dials its own container, because inside a container localhost is that container. With the value set to localhost, the test script failed the same way as in the quoted-item case, while Redis was up on the same network:

text
REDIS_HOST=localhost
ECONNREFUSED: connect ECONNREFUSED ::1:6379; connect ECONNREFUSED 127.0.0.1:6379

A Redis client library words the error its own way, but 127.0.0.1 or ::1 in it points to the app calling its own container. The ECONNREFUSED 127.0.0.1:5432 fix covers the same mistake with Postgres: point the client at the Compose service name, and gate startup on a healthcheck. The question's file also bind-mounts the project into /usr/src/app and runs npm install inside the container; the Compose development environment tutorial shows the volume that stops the host's node_modules from clobbering the container's.

The rule behind all of this is short. In an environment, build.args or labels list, a dash introduces one string, so a variable is - KEY=value. In a mapping there is no dash, and the pair is KEY: value. All four Compose releases tested reject the hybrid - KEY: value. Only the wording has changed: v1's invalid type, then must be a string, then today's unexpected type map[string]interface {}.

Further reading: Fix Docker Compose 'Exec format error' loading a library, Jenkins "Invalid agent type docker specified" Fix, Fix Docker Compose "unknown flag: --iidfile" Build Error. Also see Fix Nginx 502 in Docker Compose: upstream localhost, Fix Sequelize "Cannot find /config/config.json" in Docker.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote