Gleam on Miren
Gleam isn't auto-detected, so you deploy it with a Dockerfile.miren that compiles
your project to an Erlang shipment and runs it on the BEAM. This guide targets Erlang
(the default for server apps using mist/wisp).
Ask your AI coding agent to "set up this Gleam app on Miren" after installing the
Miren agent skills. It can add the Dockerfile.miren, confirm your
server binds 0.0.0.0:$PORT, wire up environment variables, and deploy — using this
page as its reference.
Do you need a Dockerfile?
Yes. Miren doesn't auto-detect the BEAM yet, so add a Dockerfile.miren to your
project root. Miren builds from it instead of guessing the stack — see
Using Dockerfile.miren.
The Dockerfile and app below were deployed to a live Miren cluster with Gleam 1.15 and
mist v6. Adjust the Gleam/Erlang image tags and your app name to match your project —
check
github.com/gleam-lang/gleam/pkgs/container/gleam
for available tags, and keep the runtime Erlang version in sync with the builder (see
the warning below the Dockerfile).
Miren auto-detects and builds common stacks (Python, Node, Bun, Go, Ruby, Rust) without a Dockerfile. This language isn't one of them yet — if you'd like first-class support, request it.
Bind to the injected port
Miren injects PORT and routes traffic to it, so your HTTP server must read PORT
and listen on 0.0.0.0. With mist (used directly or under wisp), read the port
from the environment — the envoy package is the usual
way. This example is validated against mist v6:
import envoy
import gleam/bytes_tree
import gleam/erlang/process
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/int
import gleam/result
import mist.{type Connection, type ResponseData}
pub fn main() {
let port =
envoy.get("PORT")
|> result.try(int.parse)
|> result.unwrap(8080)
let assert Ok(_) =
handle_request
|> mist.new
|> mist.bind("0.0.0.0")
|> mist.port(port)
|> mist.start
process.sleep_forever()
}
fn handle_request(_req: Request(Connection)) -> Response(ResponseData) {
response.new(200)
|> response.set_header("content-type", "text/plain")
|> response.set_body(mist.Bytes(bytes_tree.from_string("Hello from Gleam!\n")))
}
Bind 0.0.0.0 and use the PORT value — an app that hardcodes a port or localhost
won't receive traffic.
The mist builder API changes between major versions; on older releases the final call
is mist.start_http rather than mist.start. Add the dependencies with
gleam add mist envoy.
The Dockerfile
Create Dockerfile.miren in your project root. The erlang shipment is copied whole, so
nothing here depends on your gleam.toml name:
ARG GLEAM_VERSION=v1.15.0
# ----- Build stage -----
FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine AS builder
WORKDIR /build
COPY . .
RUN gleam export erlang-shipment
# ----- Runtime stage -----
# The Erlang major version here MUST match the OTP version in the builder image
# above, or the compiled BEAM files fail to load at startup.
FROM erlang:28-alpine
RUN apk add --no-cache ca-certificates libstdc++ openssl ncurses-libs
WORKDIR /app
COPY --from=builder /build/build/erlang-shipment ./
EXPOSE 8080
# entrypoint.sh is generated by `gleam export erlang-shipment`
CMD ["./entrypoint.sh", "run"]
gleam export erlang-shipment produces build/erlang-shipment/ containing your
compiled app, its dependencies, and an entrypoint.sh. Running ./entrypoint.sh run
boots the release.
BEAM bytecode won't load on an older Erlang than it was compiled with. The pinned
gleam:v1.15.0-erlang-alpine builder ships OTP 28, so the runtime stage uses
erlang:28-alpine. Whenever you bump the Gleam image tag, check the OTP version it
ships (docker run --rm ghcr.io/gleam-lang/gleam:<tag>-erlang-alpine erl -eval 'io:format("~s~n",[erlang:system_info(otp_release)]),halt().' -noshell) and set the
runtime image to the same major version.
.dockerignore
Keep build artifacts out of the image context:
.git
build
Set up the app
Even with a Dockerfile.miren, Miren needs at least one service defined — it
doesn't use the image's CMD as the start command. Add a Procfile next to your
Dockerfile.miren:
web: /app/entrypoint.sh run
The command is the release entrypoint the shipment generated. Use the absolute path
(/app/entrypoint.sh, matching the Dockerfile's WORKDIR /app); the script resolves
its own release directory, so it works regardless of the working directory.
Then create .miren/app.toml naming your app and deploy from your project root:
name = "gleam-bench"
miren deploy
If no service is defined, the build succeeds but the deploy stops with
no services defined: please define at least one service in a Procfile or .miren/app.toml. Defining [services.web] with the same command in app.toml
works too.
Environment variables
Set variables with miren env set — -e for plain values, -s for secrets (masked
in output and logs). Read them in Gleam with envoy.get("KEY"):
miren env set -s DATABASE_URL
miren env set -s SECRET_KEY
miren env set -s SECRET_KEY (no value) prompts with masked input. You can also declare
variables in .miren/app.toml:
[[env]]
key = "DATABASE_URL"
value = ""
required = true
sensitive = true
description = "Postgres connection string"
Need a managed Postgres database? Add a miren-postgresql addon and Miren
injects DATABASE_URL for you. See
App Configuration — Environment Variables.
Agent quick reference
- Detection: none — requires
Dockerfile.miren(Erlang shipment) - Build:
gleam export erlang-shipment→build/erlang-shipment/withentrypoint.sh - Runtime image: Erlang major version must match the builder's OTP (the pinned v1.15.0 image ships OTP 28 →
erlang:28-alpine) - Service is required: define a
Procfile(web: /app/entrypoint.sh run) or[services.web]— the imageCMDis not used - Port: read
PORTviaenvoy.get("PORT"); bindmist/wispto0.0.0.0 - mist API: v6 ends the builder with
mist.start; older versions usemist.start_http - Env vars:
miren env set -e/-s, or[[env]]inapp.toml; read withenvoy.get/1 - Database: optional
[addons.miren-postgresql]injectsDATABASE_URL
Next steps
- Using Dockerfile.miren — how custom builds work
- Addons — managed Postgres and other backing services
- App Configuration — customize
.miren/app.toml - Deployment — how deploys build and activate