Gleam Project Deployment

Deploying a Gleam application means packaging your code, compiling it for production, and running it in a reliable environment. Gleam inherits Erlang's excellent deployment story — including self-contained releases, live code upgrades, and robust process supervision.

Deployment Overview


Deployment Options
──────────────────────────────────────────────────
Option 1: Docker container
  → Most common, works everywhere
  → Package app + Erlang runtime into one image

Option 2: Erlang release (via rebar3 or mix)
  → Self-contained binary with embedded BEAM
  → No Erlang needed on the server

Option 3: Fly.io / Render / Railway
  → Platform-as-a-service with Docker support
  → Easiest for small teams

Option 4: Direct on VPS
  → Install Erlang + Gleam on the server
  → Run with gleam run or compiled release

Production Build


Development build (default):
──────────────────────────────────────────────────
gleam build
  → Fast compile, debug info included
  → Not optimized for production

Production build:
──────────────────────────────────────────────────
gleam build    # Gleam always compiles fully
               # BEAM handles runtime optimization

The BEAM JIT compiler optimizes code at runtime. Unlike many languages, there is no separate "production compile flag" — the BEAM automatically performs optimizations during execution.

Docker Deployment

A multi-stage Dockerfile keeps the final image small:


# Stage 1: Build
FROM ghcr.io/gleam-lang/gleam:v1.5.1-erlang-alpine AS builder

WORKDIR /app
COPY gleam.toml manifest.toml ./
RUN gleam deps download

COPY src/ src/
RUN gleam build

# Stage 2: Runtime
FROM erlang:26-alpine AS runner

WORKDIR /app

# Copy compiled BEAM files
COPY --from=builder /app/build/dev/erlang /app/build/dev/erlang
COPY --from=builder /root/.cache/gleam    /root/.cache/gleam

EXPOSE 8080

CMD ["erl", "-pa", "/app/build/dev/erlang/*/ebin", \
     "-eval", "my_app:main()", "-noshell"]

Docker Build and Run
──────────────────────────────────────────────────
docker build -t my_gleam_app .
docker run -p 8080:8080 my_gleam_app

Environment Variables

Read configuration from environment variables at startup:

@external(erlang, "os", "getenv")
fn os_getenv(name: charlist) -> charlist

pub fn get_env(name: String) -> Option(String) {
  let result = os_getenv(erlang.to_charlist(name))
  case erlang.from_charlist(result) {
    ""  -> None
    val -> Some(val)
  }
}

pub fn main() {
  let port =
    get_env("PORT")
    |> option.then(int.parse >> option.from_result)
    |> option.unwrap(8080)

  let host = get_env("HOST") |> option.unwrap("0.0.0.0")

  start_server(host, port)
}

Deploying to Fly.io


fly.toml
──────────────────────────────────────────────────
app = "my-gleam-app"
primary_region = "sin"   # Singapore

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8080
  force_https = true

  [http_service.concurrency]
    type = "requests"
    hard_limit = 200

[env]
  DATABASE_URL = ""   # set via: fly secrets set DATABASE_URL=...

Fly.io Deployment Commands
──────────────────────────────────────────────────
fly launch            # first-time setup
fly deploy            # deploy latest build
fly logs              # stream live logs
fly ssh console       # SSH into running instance
fly secrets set KEY=VALUE  # set environment secrets

Erlang Releases

An Erlang release bundles your application with the BEAM runtime into one directory. Copy it to any compatible Linux server and run it — no Erlang installation required on the server.


Creating a Release (using rebar3)
──────────────────────────────────────────────────
1. gleam build          # compile to BEAM files
2. rebar3 release       # package into a release
3. ./_build/default/rel/my_app/bin/my_app start

Release directory:
  bin/my_app            → start/stop/restart scripts
  releases/1.0.0/       → application code
  lib/                  → all dependencies
  erts-x.x.x/          → Erlang runtime (embedded)

Process Monitoring in Production


BEAM Built-in Introspection
──────────────────────────────────────────────────
# Connect to running node:
erl -sname debug@localhost -remsh my_app@server

# Inside the Erlang shell:
observer:start().               # GUI process monitor
:erlang.memory()                # memory usage
:erlang.system_info(:process_count)  # live process count
:sys.get_state(pid)             # actor state inspection

Health Check Endpoint

import wisp

pub fn router(req: wisp.Request) -> wisp.Response {
  case wisp.path_segments(req) {
    ["health"] ->
      wisp.json_response("{\"status\":\"ok\"}", 200)

    ["ready"] ->
      case check_all_services_ready() {
        True  -> wisp.json_response("{\"ready\":true}", 200)
        False -> wisp.json_response("{\"ready\":false}", 503)
      }

    _ -> main_router(req)
  }
}

Logging in Production


Logging Strategy
──────────────────────────────────────────────────
Development:   io.println() is fine
Production:    Use structured logging (JSON)

Recommended package: gleam_stdlib + custom logger

pub fn log(level: String, message: String, context: Dict(String, String)) -> Nil {
  let fields = dict.fold(context, [], fn(acc, k, v) {
    ["\"" <> k <> "\":\"" <> v <> "\"", ..acc]
  })
  let json = "{\"level\":\"" <> level <> "\",\"msg\":\"" <> message <> "\","
             <> string.join(fields, ",") <> "}"
  io.println(json)
}

Graceful Shutdown

import gleam/erlang/process
import gleam/otp/supervisor

pub fn main() {
  let assert Ok(sup) = start_application_supervisor()

  // Handle OS signals for graceful shutdown
  process.trap_exits(True)

  // Block until shutdown signal
  process.receive_forever()

  // Supervisor handles child cleanup automatically
  supervisor.stop(sup)
}

Key Points


Deployment Essentials
──────────────────────────────────────────────────
1. Docker is the simplest deployment target
2. Multi-stage Dockerfile: build → small runtime image
3. Read config from environment variables (PORT, HOST, etc.)
4. Fly.io, Render, Railway support Gleam/Docker easily
5. Erlang releases embed the runtime — no server dependencies
6. OTP supervisors handle restarts automatically in production
7. BEAM observer and shell give live production introspection
8. Add /health endpoint for load balancer health checks

Deploying Gleam is as straightforward as deploying any Erlang application. The BEAM's decades of production experience means your deployment story comes with battle-tested patterns for zero-downtime updates, live debugging, and automatic process recovery — features that many modern frameworks are still working toward.

Leave a Comment

Your email address will not be published. Required fields are marked *