Deployment
Copy MarkdownMooncore runs on Bandit (a pure-Elixir HTTP server) and follows standard Elixir deployment practices. There's no special deployment tooling — if you can deploy an Elixir application, you can deploy Mooncore.
Release Configuration
Environment-Specific Config
# config/config.exs — shared
import Config
config :mooncore,
router: MyApp.Router,
app_module: MyApp.App,
pools: [:default],
before_action: [MyApp.Middleware.DB],
after_action: []
# config/dev.exs
import Config
config :mooncore,
port: 4000,
mooncore_dev_tools: true,
jwt: [
key: File.read!("priv/dev_key.pem"),
issuer: "myapp-dev"
]
# config/prod.exs
import Config
config :mooncore,
mooncore_dev_tools: false # always false in production
# config/runtime.exs — runtime secrets
import Config
if config_env() == :prod do
config :mooncore,
port: String.to_integer(System.get_env("PORT") || "4000"),
jwt: [
key: System.get_env("JWT_PRIVATE_KEY") || raise("JWT_PRIVATE_KEY not set"),
issuer: System.get_env("JWT_ISSUER") || "myapp"
]
endBuilding a Release
MIX_ENV=prod mix release
Running
_build/prod/rel/my_app/bin/my_app start
Docker
FROM elixir:1.17-alpine AS build
WORKDIR /app
ENV MIX_ENV=prod
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod
RUN mix deps.compile
COPY lib lib/
COPY config config/
RUN mix compile
RUN mix release
# Runtime
FROM alpine:3.19
RUN apk add --no-cache libstdc++ ncurses-libs openssl
WORKDIR /app
COPY --from=build /app/_build/prod/rel/my_app ./
ENV PORT=4000
EXPOSE 4000
CMD ["bin/my_app", "start"]Environment Variables
| Variable | Description |
|---|---|
PORT | HTTP listening port |
JWT_PRIVATE_KEY | RSA private key PEM for JWT signing |
JWT_ISSUER | JWT issuer claim |
SECRET_KEY_BASE | For cookie signing if you use Plug sessions |
MOONCORE_DEV_TOOLS | Must be "true" to enable dev tools (used with mooncore_dev_tools: true) |
Why Two Gates?
Dev tools require both config :mooncore, mooncore_dev_tools: true AND MOONCORE_DEV_TOOLS=true. This prevents accidental exposure in production — even if a config file is misconfigured or copied from dev, the environment variable acts as a second safety gate. The config says "this environment is allowed to have dev tools" and the env var says "this specific deployment instance has dev tools turned on."
Health Checks
Add a health endpoint in your router:
get "/health" do
send_resp(conn, 200, "ok")
endFor a deeper health check:
get "/health" do
checks = %{
database: MyApp.DB.healthy?(),
pools: Mooncore.config(:pools, []) |> length()
}
status = if checks.database, do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(checks))
endProduction Checklist
- [ ]
mooncore_dev_tools: false— never expose dev tools in production - [ ] JWT private key loaded from environment variable, not committed to repo
- [ ] CORS origins restricted to your domains (not
["*"]) - [ ] Plug.Logger or custom logging configured
- [ ] Health check endpoint available for load balancers
- [ ]
start_permanent: Mix.env() == :prodin mix.exs (crashes restart the app)
Scaling
Mooncore is a single-node framework. Each instance runs independently with its own client registry and watcher state.
For horizontal scaling:
- Stateless actions work across instances behind a load balancer with no changes
- WebSocket connections are pinned to a single instance — use sticky sessions or connection-aware routing
- Pub/sub is local to each instance — for cross-node broadcasting, integrate an external pub/sub system (Redis, NATS, etc.) in a middleware or custom broadcaster
- Client registry is local — for global client awareness, sync state through an external store
Mooncore doesn't impose a clustering solution. Use what fits your infrastructure.