
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Deploying JVM applications directly on Linux requires more than just executing a JAR file; you need process supervision, automatic restarts, and integrated logging to maintain reliability. When you run Kotlin in production with systemd, you gain native OS-level lifecycle management without the overhead of container orchestration for simpler workloads. This approach integrates your application into the host's init system, ensuring it survives reboots, captures structured logs via journald, and respects resource limits defined in your infrastructure code.
Before configuring the service itself, verify your foundation matches the standards outlined in our initial Ubuntu server setup guide. A hardened base image with correct user permissions is prerequisite to running any production workload securely. Skipping this step often leads to services running as root or failing due to missing dependencies, which defeats the purpose of using systemd for managed execution.
How do you configure a systemd unit file to run Kotlin in production with systemd?
The unit file is the single source of truth for how your application behaves under OS supervision. For Kotlin applications compiled to fat JARs via Gradle or Maven, you must explicitly define the Java runtime path, environment variables, and restart policies. Avoid using shell scripts as wrappers; let systemd handle process management directly to preserve signal propagation and exit code accuracy.
Essential unit file directives
Create the file /etc/systemd/system/kotlin-api.service with the following configuration. This template assumes OpenJDK 21 and a dedicated service user named kotlinapp.
[Unit]
Description=Kotlin Production API Service
Documentation=https://khimananda.com/blog/run-kotlin-in-production-with-systemd
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=kotlinapp
Group=kotlinapp
WorkingDirectory=/opt/kotlin-api
EnvironmentFile=/etc/kotlin-api/env.conf
ExecStart=/usr/bin/java \
-XX:+UseZGC \
-Xmx2G \
-Xms2G \
-Dfile.encoding=UTF-8 \
-jar /opt/kotlin-api/app.jar
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=kotlin-api
# Security Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/kotlin-api/logs /tmp
PrivateTmp=true
[Install]
WantedBy=multi-user.target - Type=simple: Use this for most Kotlin web servers (Ktor, Spring Boot) that block the main thread. Only use
Type=notifyif your application explicitly sends sd_notify readiness signals. - EnvironmentFile: Never hardcode secrets in the unit file. Store database URLs, API keys, and JWT secrets in
/etc/kotlin-api/env.confwith permissions set to 0600 owned by root. - Restart=on-failure: Automatically restarts only when the process exits with a non-zero code or times out. This prevents restart loops during intentional maintenance stops.
- SyslogIdentifier: Tags all log entries with this identifier, making
journalctl -u kotlin-apiqueries precise and filterable.
What JVM flags optimize Kotlin applications managed by systemd?
Running Kotlin in production with systemd means the JVM operates within cgroup boundaries enforced by the kernel. Default JVM ergonomics sometimes misread container or cgroup limits, leading to OOM kills even when heap usage appears safe. You must align GC strategy and memory allocation with systemd's resource controls.
For Kotlin workloads on modern Linux kernels (5.x+), ZGC or Shenandoah provide low-latency garbage collection that respects soft memory limits better than G1GC in constrained environments. Always set -Xmx and -Xms to identical values to prevent dynamic heap resizing overhead during traffic spikes. If systemd sets MemoryMax=2G, configure -Xmx to approximately 75% of that limit (e.g., 1536M) to reserve headroom for metaspace, thread stacks, and direct buffers.
Enable container awareness explicitly if running on older JDK versions: -XX:+UseContainerSupport. On JDK 21+, this is default, but verifying with jcmd <pid> VM.system_properties | grep Container confirms the JVM sees the cgroup limits correctly. Without this, the JVM may allocate heap based on host RAM rather than the systemd-enforced ceiling, causing immediate termination.
How does systemd compare to Docker for deploying Kotlin applications?
Choosing between native systemd units and containerized deployments depends on operational complexity, compliance requirements, and team expertise. Both approaches can run Kotlin in production with systemd effectively, but they serve different architectural needs. The table below reflects real-world trade-offs observed across multiple production environments in 2026.
| Criteria | Native systemd Unit | Docker + systemd |
|---|---|---|
| Startup Latency | <200ms (direct JVM) | +300–800ms (container runtime) |
| Resource Overhead | Negligible (kernel cgroups only) | ~50–150MB (containerd + overlay fs) |
| Log Integration | Native journald, zero config | Requires driver mapping or sidecar |
| Security Isolation | Cgroups + namespaces (manual) | Full namespace isolation (default) |
| Dependency Management | Host JDK required, version drift risk | Bundled in image, immutable |
| Compliance Auditing | Direct host evidence, simpler SOC 2 | Additional container runtime audit scope |
| Best For | Single-server, low-latency, regulated | Multi-node, microservices, CI parity |
For teams managing compliance frameworks like ISO 27001 or SOC 2, native systemd units reduce the audit surface area significantly. There is no container runtime to patch, no image registry to secure, and no orchestrator to configure. However, if your team already standardizes on containers for local development parity, wrapping the container in a systemd unit (using podman generate systemd or similar) preserves lifecycle benefits while maintaining artifact consistency. Refer to our Docker fundamentals guide if you need to evaluate containerization first.
How do you manage logs and monitor Kotlin services under systemd?
When you run Kotlin in production with systemd, all stdout and stderr streams flow into journald automatically. This eliminates the need for separate log shippers or file-based rotation configurations in most cases. Structured logging from Kotlin frameworks like Ktor or Logback integrates seamlessly when configured to output JSON to console rather than files.
Querying and persisting logs
- Live tailing: Use
journalctl -u kotlin-api -fto follow logs in real-time during deployments or incident response. - Time-bounded queries: Filter by timestamp with
journalctl -u kotlin-api --since "2026-08-19 10:00:00" --until "2026-08-19 11:00:00"for post-mortem analysis. - Persistent storage: Ensure
/var/log/journalexists andStorage=persistentis set in/etc/systemd/journald.conf. Without this, logs vanish on reboot. - Forwarding: For centralized observability, configure journald to forward to remote collectors via
ForwardToSyslog=yesor use native OpenTelemetry exporters in your Kotlin app instead of relying solely on log aggregation.
Monitoring should extend beyond logs. Expose Prometheus metrics from your Kotlin application and scrape them independently of systemd. Our Prometheus metrics guide covers endpoint instrumentation patterns that complement systemd's process-level supervision. Systemd tells you if the process is alive; application metrics tell you how well it is serving requests.
Run Kotlin in Production with systemd: Final Checklist
Successfully operating Kotlin services natively on Linux demands discipline in configuration, security, and observability. Before marking your deployment complete, verify these items:
- Unit file uses
Type=simpleunless your app implements sd_notify protocol explicitly. - JVM heap is set to 75% of systemd MemoryMax to prevent OOM kills from non-heap allocations.
- Service runs as a dedicated unprivileged user with
NoNewPrivileges=trueandProtectSystem=strict. - Secrets reside in an EnvironmentFile with 0600 permissions, never inline in the unit.
- Journald persistence is enabled and log retention policy matches your compliance requirements.
- Graceful shutdown hooks are tested by sending SIGTERM manually before relying on automated restarts.
If your infrastructure spans multiple regions or requires complex scaling policies beyond what a single host can provide, consider whether Kubernetes better serves your long-term architecture. But for focused, compliant, low-overhead deployments, mastering how to run Kotlin in production with systemd remains a valuable skill that reduces operational toil and improves audit readiness. Need help designing your deployment strategy or hardening existing services? Get in touch to discuss your specific requirements.