
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Deploying BEAM applications on bare metal or virtual machines requires a process manager that understands modern Linux init systems. To run Elixir in production with systemd, you must configure a service unit that manages the Erlang runtime's unique signal handling, environment isolation, and log aggregation. This guide provides the exact configuration needed for a secure, observable, and resilient deployment on Ubuntu or RHEL-based servers.
/etc/systemd/system/myapp.service that executes your release binary via Type=notify. Configure WatchdogSec for health checks, set User=elixir for privilege separation, and use EnvironmentFile for secrets. Always enable Restart=on-failure to leverage BEAM supervision alongside OS-level recovery.How do you configure a systemd unit file to run Elixir in production with systemd?
The foundation of any reliable BEAM deployment is a correctly structured unit file. Unlike simple scripts, Elixir releases built with mix release support systemd's native notification protocol. This allows the VM to signal readiness only after all applications have started and passed their own internal health checks. For teams managing infrastructure manually or via tools like Ansible, understanding this integration is critical. If you are also managing databases on the same host, refer to our guide on PostgreSQL administration essentials to ensure your data tier matches your application tier's reliability.
Creating the Service Unit
Create the file /etc/systemd/system/myapp.service. The following configuration uses Type=notify, which is superior to Type=simple for Elixir because it prevents systemd from marking the service as "active" until the BEAM VM explicitly sends the READY=1 signal via sd_notify.
[Unit]
Description=MyApp Elixir Production Service
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=notify
User=elixir
Group=elixir
WorkingDirectory=/opt/myapp/current
EnvironmentFile=/etc/myapp/environment
ExecStart=/opt/myapp/current/bin/myapp start
ExecStop=/opt/myapp/current/bin/myapp stop
Restart=on-failure
RestartSec=5
WatchdogSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Security Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/var /tmp
PrivateTmp=true
[Install]
WantedBy=multi-user.target Key Configuration Directives Explained
- Type=notify: Requires the
:systemd_notifylibrary in your Elixir project. Without it, systemd will wait indefinitely and eventually kill the process. If you cannot add this dependency, fall back toType=execbut accept reduced readiness accuracy. - WatchdogSec=30: Enables automatic restart if the application hangs. Your Elixir app must periodically send
WATCHDOG=1notifications. This catches deadlocks that standard crash detection misses. - EnvironmentFile: Never hardcode secrets in the unit file. Use a root-owned, 600-permission file for DATABASE_URL, SECRET_KEY_BASE, and API keys.
- ReadWritePaths: When using
ProtectSystem=strict, the entire filesystem becomes read-only except for paths listed here. This is essential for compliance frameworks like SOC 2 where write-access minimization is required.
What are the best practices for securing Elixir services on Linux?
Security is not an afterthought; it is a configuration parameter. When you run Elixir in production with systemd, you inherit powerful sandboxing capabilities that isolate your application from the rest of the host. In my experience auditing deployments for Nepal-based fintechs and global SaaS platforms, most vulnerabilities stem from over-privileged service accounts rather than application code flaws.
Principle of Least Privilege
Never run your release as root. Create a dedicated system user with no shell access:
sudo useradd -r -s /bin/false -U -d /opt/myapp elixir
sudo mkdir -p /opt/myapp/var /opt/myapp/current
sudo chown -R elixir:elixir /opt/myapp Filesystem and Network Sandboxing
Modern systemd versions (v240+) support aggressive isolation. Add these directives to lock down the runtime environment:
| Directive | Purpose | Impact on Elixir |
|---|---|---|
ProtectKernel=yes | Blocks access to kernel logs and modules | Prevents information leakage; safe for standard web apps |
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX | Limits socket types | Blocks raw sockets and netlink; verify if using custom protocols |
MemoryDenyWriteExecute=yes | Prevents JIT code injection | Warning: May break some NIFs; test thoroughly first |
RestrictRealtime=yes | Blocks realtime scheduling | Safe for web/API servers; avoid for low-latency audio processing |
For deeper insights into securing the underlying operating system before deploying your application, review our Ubuntu security hardening guide. These OS-level controls complement systemd's per-service sandboxing.
How do you manage logs and observability for Elixir systemd services?
When you run Elixir in production with systemd, you should abandon file-based logging in favor of the journal. Systemd captures stdout and stderr automatically, tagging each line with the unit name, PID, and timestamp. This integrates natively with observability stacks and eliminates the need for logrotate configurations.
Configuring Logger for Journal Integration
In your config/runtime.exs, configure the logger to output plain text or JSON to stdout. Avoid file backends in production:
config :logger, :default_handler,
level: :info,
formatter: {Logger.JSONFormatter, %{metadata: [:request_id, :trace_id]}}
config :logger, backends: [:console] Querying Logs Effectively
Use journalctl with structured filters instead of grep:
journalctl -u myapp.service --since "1 hour ago"— Recent logs onlyjournalctl -u myapp.service -p err— Errors and above onlyjournalctl -u myapp.service -o json-pretty— Full metadata for debuggingjournalctl -u myapp.service _PID=12345— Filter by specific BEAM process
For centralized aggregation, forward journal entries to Loki or Elasticsearch using systemd-journal-gatewayd or Fluent Bit. Our article on structured logging best practices covers schema design for high-volume BEAM applications.
How does systemd compare to Docker or Kubernetes for running Elixir?
Choosing between systemd, containers, and orchestration depends on operational complexity, team size, and compliance requirements. There is no universal best option—only trade-offs.
| Criteria | systemd (Bare Metal/VM) | Docker Compose | Kubernetes (EKS/GKE) |
|---|---|---|---|
| Startup Latency | <100ms (direct exec) | 200–500ms (container overhead) | 1–5s (scheduler + pull) |
| Resource Overhead | Negligible | Low (~50MB base) | High (kubelet, etcd, CNI) |
| Secret Management | EnvironmentFile / Vault Agent | Docker Secrets / Env Files | K8s Secrets / External Secrets Operator |
| Rolling Deploys | Manual / Custom Scripts | Limited (recreate only) | Native (zero-downtime) |
| Audit Trail | journald (immutable) | Container logs (ephemeral) | Audit API + Pod logs |
| Best For | Single-node, compliance, edge | Local dev, small teams | Multi-region, auto-scaling |
For many Nepal-based startups and SMEs, systemd on a well-provisioned VPS offers the best balance of cost, performance, and simplicity. Kubernetes introduces significant operational tax that only pays off at scale. If you are evaluating container orchestration, our comparison of Amazon EKS provides real-world sizing guidance.
How do you handle deployments and zero-downtime restarts with systemd?
Systemd alone does not provide zero-downtime deployments, but it enables them through socket activation and graceful shutdown handling. Elixir releases support SIGTERM natively, allowing in-flight requests to complete before exit.
Graceful Shutdown Configuration
Add these directives to ensure clean stops during deploys:
TimeoutStopSec=30
KillMode=mixed
KillSignal=SIGTERM
FinalKillSignal=SIGKILL KillMode=mixed sends SIGTERM only to the main BEAM process, allowing child processes (like NIFs or ports) to receive the signal through the VM's supervision tree. After 30 seconds, systemd escalates to SIGKILL only if necessary.
Blue-Green Deployment Pattern
For true zero-downtime on a single node, use a blue-green approach with systemd:
- Deploy new release to
/opt/myapp/green - Start
myapp-green.serviceon a different port - Run health checks against the green instance
- Update Nginx/HAProxy upstream to point to green
- Stop
myapp-blue.servicegracefully - Promote green to current symlink
This pattern avoids complex orchestration while delivering production-grade reliability. It pairs well with the blue-green deployment strategies used in containerized environments, adapted for bare-metal constraints.
Production Readiness Checklist
Before marking your Elixir service as production-ready, verify these items:
- Unit file uses
Type=notifywith working sd_notify integration - Service runs as non-root user with
NoNewPrivileges=true - Secrets loaded via
EnvironmentFile, not hardcoded WatchdogSecconfigured and tested with simulated hangs- Logs flowing to journald with structured format
- Graceful shutdown tested under load with
systemctl stop - Filesystem write paths explicitly whitelisted
- Monitoring alerts configured for service failures and watchdog timeouts
To run Elixir in production with systemd successfully, treat the unit file as infrastructure code: version it, review it, and test it. The BEAM VM is remarkably resilient, but it depends entirely on the OS layer to handle failures it cannot see. If you need help designing a compliant, observable deployment pipeline for your Elixir application, get in touch to discuss your architecture.