Troubleshooting
The gotchas that actually bite, and how to diagnose a service that will not start. When something is wrong, two commands tell you most of what you need:
eph check # does the .eph file parse?
eph -v up # start with verbose (debug) logging on stderr
“failed to connect to docker (is docker running?)”
eph talks to your local Docker daemon. Start Docker (Docker Desktop, Colima,
systemctl start docker, whichever you use) and confirm with docker ps. On
macOS and Windows, make sure the Docker Desktop VM has fully started, not just
begun launching.
”no .eph file found in … or any parent directory”
You are not inside a workspace. eph searches the current directory and walks
up to find a .eph file. Either cd into your project or create a .eph
file. Confirm what eph resolves with eph info.
A service fails to become healthy
service postgres failed to become healthy within 30s
Causes, in rough order of likelihood:
- The health check uses shell features. For
imageanddockerfileservices the health check runs inside the container without a shell: it is split on whitespace and exec’d directly. Pipes (|),&&, redirects,$VAR, and quoted arguments containing spaces do not work. Use one plain command, such aspg_isready -U devorredis-cli ping. (Forrunandcomposeservices the check runs through the platform shell, so shell syntax is fine there.) - The check binary is not in the image.
mysqladmin,mongosh,pg_isready, andcurlmust exist inside the container. Slim images may omit them. - The service genuinely needs longer. Raise
ready-timeout=(seconds). - The service crashed on startup. Inspect its logs with
eph logs <service>, which works for every service type and shows arun=service’s output even after it exited. For Docker-backed services you can also go straight to the daemon:docker logs eph-<short_id>-<service>(compose the name fromeph infoplus the service name, or find it withdocker ps -a).
Run eph -v up to watch each health-check attempt and its exit code.
A property was rejected
Any typo’d or misplaced service property is a hard parse error; eph check
catches it before any eph up. Two shapes show up in practice:
-
A lowercase typo is an unknown-property error listing every known property name:
unknown service property 'prot' at line 5 (known properties: image, dockerfile, compose, run, role, command, port, port.<name>, expose.<name>, env.<KEY>, volume, pre-start, post-start, pre-stop, post-stop, healthcheck, ready-timeout, context)Property names are lowercase:
image,port,env.X,healthcheck,post-start, and so on. -
An UPPERCASE key inside a section (the classic
HEALTHCHECK=...instead ofenv.HEALTHCHECK=..., or a top-level variable you meant to put after this service) is rejected rather than silently absorbed as a top-level variable:'HEALTHCHECK' at line 5 looks like an environment variable, but it is inside service 'postgres' (sections do not end at blank lines). To set it for the service, write env.HEALTHCHECK=...; to export it from `eph env`, move it into an [env] section or above the first sectionThe error names both possible intents so you can pick the right fix: prefix it with
env.if it belongs to the service, or move it into an[env]section (or above the first section) if it belongs to your shell. See Where to put top-level variables.
Something is “duplicate”
Nothing in a .eph file is silently merged or overwritten. A repeated
declaration is a parse error naming both occurrences:
- A reopened section (
[db]appearing twice) does not merge the two blocks; the second[db]is rejected, naming the line the first one started on. - A repeated single-valued property (a second
image=,healthcheck=,command=,ready-timeout=, or a second source under any spelling, such asimage=followed byrun=) is rejected. Hooks andvolume=are the exception: they are designed to repeat and accumulate. - A repeated port or
env.key (port=twice, the sameport.<name>=twice, the sameenv.KEY=twice) is rejected; give each a distinct name instead. - A repeated top-level variable name is rejected, whether both
declarations are above the first section, both inside
[env]sections, or split across the two: the top-of-file block and every[env]section share one namespace.
Give the second occurrence a different name, or delete it if it was a leftover from editing.
An interpolation reference did not parse
${service.property} placeholders in a top-level variable or env.<KEY>=
value are validated at parse time, before any eph up:
unterminated '${' ...: a${with no closing}. Close it, or if you meant a literal${, escape it as$${.invalid interpolation ... expected ${service.property}: the text inside${...}has no., so it cannot name a service and a property (for example${name}instead of${web.port}). Add the missing part, or escape it as$${if it was never meant to be a placeholder.unknown service '...' referenced from ...: the placeholder names a service that is not defined anywhere in the file. Check the spelling against the section header; a service defined later in the file resolves fine, so this is always a genuine typo or a missing section.
These are different from a port reference that does not resolve: that is a well-formed reference to a real service that is not running.
An inline comment broke a value
Comments must be on their own line. There are no trailing comments; a #
after a value is part of the value:
port=5432 # this whole thing is the value, and fails to parse
Symptoms: invalid port number at line N, or an image name or URL that
mysteriously has # ... appended. Move the comment to its own line above.
pre-start or post-start ran again and broke (or duplicated data)
pre-start and post-start hooks run on every eph up, including when a
stopped container is restarted, not just on first creation. A hook that is not
idempotent (a plain INSERT seed, a one-shot setup script) repeats its effect
and may fail or duplicate rows on the second eph up.
Fixes:
- Make the hook idempotent: a migration that no-ops when already applied, an
INSERT ... ON CONFLICT DO NOTHINGseed,CREATE TABLE IF NOT EXISTS, codegen that overwrites its output in place. - Move one-off or destructive work out of the hook and run it explicitly with
eph runwhen you actually want it.
eph down or eph clean fails on a pre-stop or post-stop hook
A failing pre-stop hook aborts the teardown and leaves the service
running, so the hook (a backup, a drain) can be fixed and retried rather than
silently skipped. A failing post-stop hook also aborts the rest of the
teardown, but its own service is already stopped, so re-running eph down
will not run that post-stop again. If a broken hook is wedging teardown:
- Fix the hook, then re-run
eph downoreph clean. Apre-stopretries on the re-run; apost-stopwhose service already stopped must be run by hand, for example witheph run. - Or skip the hooks for this teardown:
eph down --skip-hooksoreph clean --skip-hooks.
A port reference did not resolve
eph check rejects unknown services, unknown properties, missing named ports,
and ambiguous bare port references. A valid reference can still be unavailable
at runtime when its service is stopped. In that case, eph env unsets the
affected shell variable or omits the JSON key, warns on stderr, and exits
non-zero. Other execution paths fail before launching a child.
- The service is not running. Interpolation only resolves against running
services. Run
eph upfirst, and check witheph status. - The name is wrong.
${db.port}only resolves if the section is[db];eph checkreports this before runtime. - It is a multi-port service.
${minio.port}is rejected when a service declares several ports; use${minio.port.api}. Compose mappings useexpose.<alias>=<compose-service>:<port>and resolve as${service.port.<alias>}.
A run= service is on the wrong port
With a numeric port=, a run= service is not port-remapped: the process
binds whatever it binds, and eph reports the declared value verbatim for
interpolation. Make the declared port match the port your process actually
listens on, or switch to port=auto and let eph allocate and inject it (see
Running Your App). If you use port=auto and the
app still lands elsewhere, your framework is ignoring its injected PORT;
enable its strict-port mode.
Stale state or “ghost” services
eph status reconciles saved state against the live Docker daemon and tracked
PIDs, so a container you removed manually simply drops out of status. If
state ever looks wrong, eph clean resets the workspace completely (removing
containers, named volumes, and the state file), after which eph up rebuilds
from scratch.
Renaming or deleting a service’s section from the .eph file does not orphan
its container: a bare eph down and eph clean both also tear down whatever
state.json remembers starting under a name no longer in the file. If you
still find a container docker ps -a shows but eph status does not, run
eph clean, which additionally sweeps any leftover container or volume
carrying the workspace’s eph-<short_id>- prefix even if state does not know
about it.
”state file … is corrupt”
If state.json cannot be parsed (a hand edit, disk corruption, damage from
something outside eph), the next eph command logs a warning, moves the
broken file aside to state.json.corrupt, and continues as if the workspace
had no recorded state, rather than aborting. Containers and compose projects
are found again from Docker by name on the next eph up or eph status; a
run= service’s PID is the one thing this cannot recover, since the PID lived
only in the corrupted file. Inspect the host process table and stop a leftover
process by hand; eph status and docker ps cannot discover it.
”another eph command is running in this workspace; waiting for it”
eph up, eph down, and eph clean on one workspace exclude each other with
an OS-level lock, so two overlapping runs never race the same state.json.
This message just means a second command has to wait; it clears on its own
once the first command finishes. If it never clears, the first command is
still genuinely running (check eph -v up’s output or docker ps) rather
than stuck: the lock is an OS file lock released automatically when the
holding process exits, so a crashed or killed eph cannot leave a later
command wedged.
docker compose down failed during eph down or eph clean
A compose service’s teardown failure, such as a missing docker compose
plugin, is a real error and stops the rest of the teardown. eph tears down by
recorded project name without rereading the Compose file:
docker compose -p eph-<short_id>-<service> down reproduces the command.
Fix the underlying problem and rerun eph down or eph clean. --skip-hooks
does not help here; it only skips lifecycle hooks.
If the workspace directory itself was deleted, run eph system prune from
anywhere. It scans all eph state directories and removes resources for
recorded workspace paths that are missing or empty folders. Use eph system prune --dry-run first to see the plan. If a recorded path still contains files
but you know the workspace can be discarded, preview it with
eph system prune --force-non-empty --dry-run, then rerun with
eph system prune --force-non-empty --yes. Add --force-live when the preview
reports running containers or a live run= process. An 8-character state
directory without workspace metadata is skipped unless you pass
--compatibility-v042.
For run= services, system prune stops only a recorded PID whose live process
still matches the identity eph captured at launch; PIDs can be reused, so a
mismatch is skipped with a warning rather than killed. A command that
deliberately detached grandchildren outside the shell tree eph launched can
still leave processes behind after that tree exits; stop those manually.
run= teardown says the PID has no recorded process identity
A run= state entry without process identity cannot distinguish its PID from a
reused PID. eph down, eph clean, and config reconciliation refuse to signal
a live PID in that state. Inspect the process, stop it manually if it belongs to
the workspace, and rerun the eph command. Every launch must capture a stable
identity; startup stops the child and fails when capture is unavailable.
Windows
eph runs natively on Linux, macOS, and Windows. Docker-backed sources use the
local Docker daemon and platform path conventions.
The shell-based features (run= services, lifecycle hooks, and run/
compose health checks) run through the platform shell: sh -c on Unix and
cmd /C on Windows. Process liveness and teardown are native (no POSIX
kill required), and teardown stops the whole process tree a run= command
spawned, so a compound command’s children are not orphaned. The features
work natively on Windows with no WSL.
What does not cross over automatically is the command string itself: a run=,
hook, or health-check command written for sh (pipes, $VAR, &&, POSIX
tools) may need a cmd-compatible rewrite on Windows (%VAR%, different
builtins). Two ways to handle it:
- Write the command for
cmd, or call a cross-platform binary directly; many, likepg_isreadyandredis-cli, take the same arguments on both platforms. - Keep your POSIX command strings and run
ephinside WSL, where it is a Linux process andshis the shell.
Under WSL, eph is a Linux process, so its state directory is the Linux
path (~/.local/share/eph/<short_id>/), not the Windows %LOCALAPPDATA%
path. The %LOCALAPPDATA% location applies only to a native Windows build.
(EPH_STATE_ROOT overrides either default; see
Core Concepts.)
Relative bind mounts on Windows
A relative host bind mount (volume=./seed:/docker-entrypoint-initdb.d)
resolves against the workspace root and is passed to Docker as a plain
C:\... path. Absolute drive-letter paths (C:\..., C:/...) and UNC paths
are accepted. Docker does not accept an extended-length \\?\... bind source;
eph rejects that form before container creation. Move a workspace whose
resolved path requires that prefix to a shorter path.
Getting more detail
eph -v <command> enables debug logging (to stderr). For a service’s own
output, use eph logs <service> (add -f to follow). For Docker-level
issues, drop to the docker CLI using the names from eph info:
docker ps -a | grep eph-<short_id> # containers for this workspace
docker logs eph-<short_id>-<service> # a service's logs (or: eph logs <service>)
docker volume ls | grep eph-<short_id> # named volumes for this workspace
Next
The Command Reference has every command and flag in one place.