There’s a strong tendency to want to run everything in Docker these days,
especially if you’re trying to run something as an always-on service, since
--restart=always to your
run invocation or Docker Compose
configuration ensures that running containers start back up after reboots or
failures, and seems to involve a little less “black magic” than actually
configuring software to run as services directly on a host.
The downside to this is the approach is that running a service in a container leads to significantly longer startup times, more memory and CPU overhead, lost logs, and in my opinion offer a false sense of security and isolation since most images are still configured to run as root, and more often than not large swathes of the host filesystem are mounted as volumes to achieve simple tasks.
There’s also a belief that your software will magically run anywhere - but if you’re writing Java (or any JVM language) code - that’s one of Java’s biggest selling points - it already has its own VM your code is running in, no most platforms!
Therefore, let’s see how easy it actually is to configure our software to run
as a standard system service, providing us with the ability to run it as a
separate restricted user, complete with standard logging configuration, and
give us control over via standard
service myservice start|status|restart|stop
For the below, I’m assuming we’re on a modern Debian-like Linux. Although I describe and refer to deploying Java services here, the same process can be used for any binary which runs on your target infrastructure.
Create a dedicated user
We’ll create a user to run our service as, so it can’t tamper with system-owned
resources, and its own configuration may remain private. We’re creating a
dedicated group, as well as user which cannot log in with a valid shell, named
$ sudo groupadd -r svcuser $ sudo useradd -r -s /bin/false -g svcuser svcuser
Deploy software to server
Let’s copy our service binaries to the server, and make sure they’re owned by
the new user. I like to install these services under
$ sudo mkdir /opt/myservice # put your jar files, libraries, or whatever into /opt/myservice $ sudo chown -R svcuser:svcuser /opt/myservice
If you’re migrating from Docker or Docker Compose, you likely have your service
configured via environment variables. We can continue using these via an
EnvironmentFile, or you can create a configuration file if your service
supports reading one.
As such, this step will vary depending on your service implementation.
If you’re using environment variables, define an
EnvironmentFile in the
If your configuration file contains sensitive information, you may restrict it
to being read by the service only, via the following, which will ensure it’s
only readable by
root and the
$ sudo chown svcuser /etc/myservice/config.env $ sudo chmod 0640 /etc/myservice/config.env
Define the SystemD service
Create this file in
[Unit] Description=My Service After=syslog.target After=network.target [Service] WorkingDirectory=/opt/myservice/ ExecStart=/usr/bin/java -jar /opt/myservice/myservice.jar EnvironmentFile=/etc/myservice/config.env User=svcuser Type=simple StandardOutput=syslog StandardError=syslog SyslogIdentifier=myservice SuccessExitStatus=0 TimeoutStopSec=120 Restart=always [Install] WantedBy=multi-user.target
EnvironmentFile is only needed if your config is defined using
systemd about our new service:
$ sudo systemctl daemon-reload
It’s important to remember to do the above
reload whenever making changes to
a service definition.
You’ll see above that we configured output to be written to
syslog, and that
we defined a
SyslogIdentifier with a unique name for our service.
With this configuration, we can configure
rsyslog to write logs to our own
$ sudo vim /etc/rsyslog.d/myservice.conf
Add the following to the above configuration file. In this case
corresponds to the
SyslogIdentifier we configured in the service, so any
logging will be redirected to it’s own log file.
if $programname == 'myservice' then /var/log/myservice.log & stop
Prepare log file permissions:
$ sudo touch /var/log/myservice.log $ sudo chown svcuser:svcuser /var/log/myservice.log
rsyslog with the updated configuration:
$ sudo service rsyslog restart
If your service generates a lot of logs, log rotation could also be configured
at this point, by placing an entry in
/etc/logrotate.d/. Refer to the other
configuration files in that directory to get a sense of how they work.
Start it up!
You can now control your service:
$ sudo service myservice start $ sudo service myservice status
You can also inspect or follow its logs:
$ sudo tail -f /var/log/myservice.log
Enable or disable at startup
If you want your service to automatically start each time the host reboots,
enable the service as follows:
$ sudo systemctl enable myservice
Similarly, to prevent services from starting up (this can also be handy for other services, like a DB or web service you’ve installed for development purposes but don’t need or want running all the time):
$ sudo systemctl disable myservice
A disabled service can still be manually started and stopped with the
service myservice [start|stop|restart|status] commands.
The above likely seems to be a rather lengthy process as I’ve laid it out, but it’s probably 5-10 minutes “work” in total, and you get a result much better integrated into the host system, easily manageable logs, plus it’s easier to monitor, manage and debug - especially for services you’re developing and would like to know more about how it’s behaving, without the complexity of additional layers between you and your code.
This will also work perfectly well for non-Java services, simply adjust the
ExecStart parameter as required for your other applications.