TL;DR

Goss tests the image, not the running container. Use dgoss run --env-file .env to inject your environment and then assert in three layers: does the var exist, is it non-empty, and does it actually authenticate? That layering tells you exactly where the chain breaks instead of just “MySQL connection failed.”

The problem

I saw someone in a DevOps forum wrestling with this exact thing. They were manually debugging why their .env values weren’t translating properly into MySQL credentials, and had turned to Goss to automate sanity checks. Two questions tripped them up:

  1. Can dgoss run against a live container, or does it only work against images?
  2. How do you actually write test cases that catch credential failures?

Both are solvable, and the answers come down to understanding the right mental model first.

Test the image, not the container

Goss’s unit of work is the image. You build an image, then you assert that a container spawned from that image has exactly what it should: the right binaries, the right files, the right ports open. The command for this is:

dgoss run your-image:tag

This spins up a fresh container, runs your goss.yaml spec inside it, then tears it down. It’s intentionally ephemeral.

There’s no dgoss exec that drops into a running container. That’s by design — if you’re testing a running container, you’re testing mutable state. The image is your immutable artifact. Test that.

The one real exception: if you need to assert runtime behavior that depends on something the container does after startup (initializing a schema, writing a PID file), you can use dgoss run with a --wait flag and a goss_wait.yaml. But for the credential assertion problem, you don’t need it.

The .env translation problem

The real question isn’t “does the env var exist” — it’s “does this env var contain a value that authenticates against the database.” Those are three different things, and they fail in three different ways:

FailureSymptomGoss layer that catches it
Var not passed at all$MYSQL_PASSWORD is unsetenv resource check
Var is empty stringVar exists, value is ""command check with [ -n "$VAR" ]
Var has wrong valueVar set, MySQL rejects itcommand check with real mysql call

If you only check layer 1, you’ll miss layers 2 and 3. If you only check layer 3, you won’t know why it failed.

Writing the test spec

Here’s the full goss.yaml for this pattern. You can download it below.

# Verify the MySQL client exists in the image
file:
  /usr/bin/mysql:
    exists: true

# Layer 1: vars are present at all
env:
  MYSQL_HOST:
    exists: true
  MYSQL_USER:
    exists: true
  MYSQL_PASSWORD:
    exists: true
  MYSQL_DATABASE:
    exists: true

# Layer 2: vars are non-empty
# Goss doesn't have a native "not empty" check on env, so use a command
command:
  check-host-not-empty:
    exec: '[ -n "$MYSQL_HOST" ]'
    exit-status: 0
  check-user-not-empty:
    exec: '[ -n "$MYSQL_USER" ]'
    exit-status: 0
  check-password-not-empty:
    exec: '[ -n "$MYSQL_PASSWORD" ]'
    exit-status: 0
  check-database-not-empty:
    exec: '[ -n "$MYSQL_DATABASE" ]'
    exit-status: 0

# Layer 3: the credentials actually work
command:
  mysql-connectivity:
    exec: "mysql -h $MYSQL_HOST -u $MYSQL_USER -p$MYSQL_PASSWORD -e 'SELECT 1' $MYSQL_DATABASE"
    exit-status: 0
    timeout: 5000

Download goss.yaml

Running it

Pass your .env file to dgoss run:

dgoss run --env-file .env your-image:tag

dgoss passes --env-file straight through to docker run. Your .env is mounted into the test container, so when the mysql command in layer 3 executes, $MYSQL_PASSWORD is the actual value from your file.

If you’re on Podman instead of Docker:

DGOSS_DOCKER_CMD=podman dgoss run --env-file .env your-image:tag

This is documented nowhere obvious. Goss shells out to docker by default — set DGOSS_DOCKER_CMD to override it. You can also export it in your shell profile if your whole workflow is Podman-based.

Reading the output

When layer 3 fails but layers 1 and 2 pass, you know the value is present and non-empty but wrong. That narrows it immediately: typo in the password, wrong user permissions, MySQL not accepting connections from the container’s IP, or the host resolving to something unexpected.

When layer 2 fails, your .env file has a key with no value — probably MYSQL_PASSWORD= with nothing after the equals sign. Common when secrets management systems emit empty vars on failures.

When layer 1 fails, the var wasn’t passed at all. Either the --env-file flag was omitted, the key is missing from .env, or there’s a typo in the key name.

Each failure mode points to a different fix. That’s the point of layering.

What about the goss_wait.yaml pattern?

If MySQL isn’t started yet when Goss runs the connectivity check, you’ll get a false negative. For integration testing where the database is part of the compose setup, use goss_wait.yaml to poll until it’s ready:

# goss_wait.yaml — Goss polls this until it passes or times out
tcp:
  mysql:
    address: "$MYSQL_HOST:3306"
    reachable: true
    timeout: 30000
GOSS_WAIT_OPTS="-r 30s -s 1s" dgoss run --env-file .env your-image:tag

For the credential assertion use case (testing whether your vars are right, not whether MySQL is up), you don’t need this — run it against a cluster where MySQL is already running and just assert the auth.

Lessons

  • Test the image, not the container. Mutable state is untestable. The image is your artifact.
  • Layer your assertions. Exists → non-empty → actually works. Don’t skip to layer 3 and wonder why it broke.
  • Podman users: set DGOSS_DOCKER_CMD. It’s an environment variable, not a flag. Put it in your Makefile or shell profile.
  • dgoss is a thin wrapper. If you understand docker run, you understand dgoss. When in doubt, read what it’s passing to docker.

Don’t have a homelab? This same testing pattern works anywhere you’re running containers. A DigitalOcean account with a small Droplet is enough to run these workflows.