Create a user and a folder
sudo useradd --system --home-dir /opt/app --shell /usr/sbin/nologin app
sudo mkdir -p /opt/app
sudo install -o app -g app -m 640 myapp.jar /opt/app/app.jar
The service runs as an unprivileged user without a login shell. A bug in your application then cannot change the system.
Environment file
# /etc/app.env (chmod 640, owner root:app)
SERVER_ADDRESS=127.0.0.1
SERVER_PORT=8080
SPRING_PROFILES_ACTIVE=prod
SPRING_DATASOURCE_URL=jdbc:postgresql://127.0.0.1:5432/appdb
SPRING_DATASOURCE_USERNAME=app
SPRING_DATASOURCE_PASSWORD=change-me
JAVA_OPTS=-Xms1536m -Xmx1536m -XX:MaxMetaspaceSize=256m -XX:+ExitOnOutOfMemoryError
Spring Boot maps environment variables to properties, so SERVER_PORT becomes server.port. Secrets stay out of the JAR and out of your repository.
The unit
# /etc/systemd/system/app.service
[Unit]
Description=My Java application
After=network.target
[Service]
User=app
Group=app
EnvironmentFile=/etc/app.env
WorkingDirectory=/opt/app
ExecStart=/bin/sh -c 'exec java $JAVA_OPTS -jar /opt/app/app.jar'
SuccessExitStatus=143
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Exit status 143 is what a JVM returns after SIGTERM, so a normal stop is not reported as a failure.
Start it
sudo systemctl daemon-reload
sudo systemctl enable --now app
sudo systemctl status app
sudo journalctl -u app -f
Deploy a new version
sudo cp /opt/app/app.jar /opt/app/app.jar.prev
sudo install -o app -g app -m 640 /tmp/myapp.jar /opt/app/app.jar
sudo systemctl restart app
On our servers deploy-app does exactly this. To roll back, copy app.jar.prev over app.jar and restart.
Next
Put Nginx in front for HTTPS: Nginx reverse proxy for a Java app.
Last updated October 29, 2026.