Elixir in WSL: Mix Projects, OTP Processes, and Linux Dependencies
Develop Elixir applications in WSL with explicit Erlang and Elixir versions, Mix dependencies, OTP supervision tests, and Linux-native release artifacts.
Elixir on the Erlang virtual machine is a natural fit for a Linux control environment under WSL. It lets developers compile, test, and package applications with the same operating-system family used by many deployments while keeping the Windows host available for editors and terminals. The boundary still matters: Windows and Linux runtimes use different executables, paths, native libraries, and process environments. A successful test under Windows Elixir is not proof that a Linux release can start in WSL or on a server.
Treat the runtime version, dependencies, native extensions, configuration, and release target as separate inputs. Mix manages much of the project workflow, and OTP supplies the process and supervision model, but neither turns one local distribution into a fault-tolerant production system.
Install a compatible Erlang and Elixir pair
Choose versions from the application’s compatibility policy and the official Elixir installation guidance. Elixir runs on Erlang/OTP, so recording only elixir --version is incomplete. Capture both language and OTP versions, architecture, and executable paths. A newer Erlang runtime may be unsupported by a project’s Elixir version or dependency set, even if the shell launches successfully.
Keep the source tree, build output, dependency checkout, and any native compilation cache in the WSL Linux filesystem. Microsoft recommends the distribution filesystem for Linux command-line workloads. Do not share _build or deps directories with a Windows build. NIFs and native dependencies are compiled for a specific platform and ABI; use separate checkouts or clean build profiles when testing Windows and Linux targets.
Confirm the runtime before a test or release build:
command -v elixir
elixir --version
command -v mix
mix --version
Make the same checks from the editor or task runner that will build the application. An interactive terminal can inherit a version-manager initialization script that a non-interactive process does not source. Avoid relying on a global PATH that silently selects different runtime versions in different WSL distributions.
Use Mix as the project boundary
Mix reads a project’s mix.exs, dependencies, aliases, and build configuration. Start in the repository root and inspect the project definition before resolving packages. mix deps.get fetches declared dependencies; mix deps and the lockfile help review the resolved graph. Keep credentials out of mix.exs, shell history, and committed configuration. If private Hex packages or Git repositories are used, document the controlled authentication path separately.
For a bounded development cycle:
cd "$HOME/src/my_app"
mix deps.get
mix compile --warnings-as-errors
mix test
Treat warnings-as-errors as a project choice, not a universal default; some dependencies may emit warnings that are not owned by the application. Run the project’s prescribed command and compare the test environment with CI. Tests that require a database, message broker, or external service should start a disposable dependency and assert readiness rather than racing a fixed sleep.
Mix environments such as dev, test, and prod select compile-time and runtime behavior. A test passing under MIX_ENV=test does not validate production configuration. Review environment variables, release configuration, runtime configuration files, and secret injection for the actual launch path. Avoid compiling production code with development-only dependencies and then treating the artifact as equivalent to a release build.
Test OTP process and supervision behavior
OTP applications are built from processes and supervision trees. Test the behavior that the supervision strategy promises: a child restart after a controlled failure, state recovery, shutdown handling, and message flow. A process being alive in a unit test does not prove a multi-node deployment survives a host or network failure. Keep test processes isolated and use deterministic timeouts rather than relying on scheduler timing.
When an application starts an HTTP listener, database pool, or background consumer, validate the startup contract separately from module tests. Check which port and interface it binds, what configuration it loads, and whether shutdown drains or terminates work as expected. WSL sleep, distribution shutdown, and Windows host restart are outside the application supervision tree; design the application to recover when its OS process starts again, but do not claim continuous uptime from a workstation VM.
Mix aliases can make project-specific checks concise, but inspect their definitions before invoking them in automation. An alias may compile assets, migrate a database, or contact an external service. Use narrow test targets while iterating, then run the full CI command against disposable data. Record the exact command, environment, and OTP release used for a failure report.
Native dependencies, NIFs, and releases
Some dependencies compile NIFs or invoke C toolchains and system libraries. These must be available and compatible in Linux. If compilation fails, distinguish missing headers from a version mismatch or a package-specific build error. A Windows precompiled dependency is not reusable by the WSL virtual machine. Inspect the dependency’s supported platform and build instructions rather than copying binary files across the OS boundary.
Mix releases package an application for deployment, but the release target and runtime requirements must match the destination. Review the official release documentation for included Erlang runtime behavior, configuration, and startup scripts. Build and start a test release in a disposable directory; verify logs, health checks, configuration overrides, and graceful shutdown. Do not assume a release built for the workstation will run on a different CPU architecture or Linux distribution without validation.
Keep release output and mutable state separate. Configuration secrets should be injected through an approved runtime mechanism, and database files or uploaded content should not be stored inside an immutable release tree. Use a dedicated path for local data and test backup/restore independently. If the distribution stops, local state remains subject to the WSL VHDX’s durability and host lifecycle.
Runtime configuration and operational evidence
Separate compile-time configuration from runtime configuration. Compile-time values can become embedded in build output, while runtime configuration is evaluated as the application starts. A test that reads an environment variable in dev may not prove that a release sees the same variable. Verify the exact release startup path, environment variable names, and configuration file location in a disposable environment. Redact secrets from startup logs and avoid relying on shell history for credentials.
For process-level diagnostics, preserve the first failing stack trace and the relevant supervision context. A child restart can be correct recovery behavior or a loop that hides a persistent dependency failure. Record restart frequency, reason, and whether the application became ready after restart. Tests should distinguish expected transient recovery from crash loops and should put bounds on retry behavior. A supervisor can restart a process; it cannot make an unavailable database or a stopped WSL VM healthy.
Exercise graceful shutdown for applications that hold resources or consume work. Confirm whether in-flight operations finish, are retried, or are abandoned when the process receives its shutdown signal. Long-running WSL tasks can be interrupted by host sleep, terminal closure, or explicit distribution shutdown. Persist durable business state outside ephemeral process memory and use an external service for work that must survive workstation lifecycle.
Native dependencies should be listed as build prerequisites, not inferred from a developer’s machine. For a clean release check, use a separate build directory or fresh checkout and install only documented Linux development packages. If the application embeds a runtime, inspect the generated release structure and test on a compatible Linux target. Keep target architecture and OS release in the artifact record.
Troubleshooting and acceptance
If dependencies fail to compile, compare Elixir, OTP, compiler, headers, and architecture. If tests pass locally but fail in CI, compare environment, locale, timezone, generated assets, and service readiness. If a release cannot start, inspect the release’s selected target, runtime configuration, shared libraries, and logs before changing the supervision tree. If the application stops when WSL closes, distinguish VM lifecycle from an OTP crash.
Accept the WSL Elixir setup when the Erlang and Elixir versions are recorded, the project and build artifacts stay Linux-native, dependencies resolve reproducibly, tests exercise supervision and failure paths, and a test release starts and stops with the intended runtime configuration. Keep production-like external services disposable and separate from user data.
Elixir and OTP in WSL provide a useful Linux development and packaging environment. They do not make local tests a substitute for production cluster testing, release compatibility checks, or service-level availability engineering.
Related:
- RabbitMQ in WSL: A Local Messaging Reliability Lab
- Temporal in WSL: Local Workflow Development and Replay Tests
Sources: