Deploying a Go binary using systemd in Ubuntu

Although Docker and container orchestrators like Kubernetes have solved the issues such as ensuring that a web service keeps running all the time, they are quite complicated to configure and maintain for small SAAS applications and side projects. For my own side projects such as https://sslnotify.com/, I use a systemd service to deploy my Go binary. This setup is very simple and it serves my critical needs such as ensuring that the web server is always running and also provides indexable logging using journald. In this tutorial, we will learn how to run a Go binary as a systemd service in Ubunutu.

What is Systemd?

Systemd is a set of tools that provide easy management of services, sockets and so on in Linux. In this tutorial, systemd will be used to manage a Go webserver. A service can be any binary that needs to be up and running all the time. The most important components of the Systemd software suite are the systemd daemon, unit files and the systemctl CLI utility which is used to query systemd.

A process that is controlled by systemd is configured using a unit file with the .service extension.

Structured logging is provided with the help of a daemon called journald and it can be queried using journalctl CLI utility.

Although these components seem overwhelming right now, the use of each them will be clear as the tutorial progresses.

Go program

The following is the Go program that will be run using a systemd service.

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7	"time"
 8)
 9
10func handler(w http.ResponseWriter, r *http.Request) {
11	fmt.Fprintf(w, "Go is awesome!")
12}
13
14func main() {
15	http.HandleFunc("/", handler)
16	go func() {
17		time.Sleep(30 * time.Second)
18		log.Fatal("Shutting down server after 30 seconds")
19	}()
20	log.Fatal(http.ListenAndServe(":8005", nil))
21}

This program is deliberately terminated after 30 seconds in line no. 18 to simulate a runtime panic. This program can be run using the following commands

go build -o test-server
./test-server

After the above commands are run, the webserver will be up and running in port 8005. The command

curl localhost:8005

will print Go is awesome!.

This confirms that the server is running.

After 30 seconds, the web server will terminate with the message

2026/07/05 00:41:37 Shutting down server after 30 seconds

Now if curl localhost:8005 is run again, the command will print the error

1curl: (7) Failed to connect to localhost port 8005 after 0 ms: Connection refused

since the server is not running anymore.

We will ensure that the webserver is running always using systemd. The webserver will be configured using a systemd unit that will restart the server once it terminates. In simple words, a systemd unit is a file with extension .service which controls a process. In this case, it will control the Go binary process which runs the web server. We will create this file as the tutorial progresses.

Creating the Go binary

The prerequisite for the systemd service is the availability of the Go binary. The following command will create a Go binary named test-server.

1go build -o test-server

After the binary is created, it will be copied to /opt, a standard path in linux where applications exist.

1sudo mkdir /opt/test-server
2sudo cp ./test-server /opt/test-server/

Creating the user for the systemd unit

The next step is creating a new user which will be used to run the test-server. By default, a systemd unit runs as the root user. If the server is run as root, the blast radius will be huge in case there is a security incident due to a compromised web server. As a security best practice, the web server should run using a user account with only the minimum privileges required for its operation. This will reduce the blast radius in case there is a security vulnerability and a bad actor is able to control the webserver.

The following commands creates a user and group foo which will be used to run the test-server systemd unit.

1sudo adduser --system --no-create-home --group foo

The above command creates a new user named foo with no home directory, no shell and it also creates a new group foo. The user foo will not be able to login interactively. If anyone tries to login using the user foo, the system will reject it. This user is perfect for running our web server. It is not over privileged and it has the minimum privileges necessary to run the web server.

Creating the systemd unit

Each systemd unit is represented by a file with a .service extension. A unit is used to manage a service, in our case the web server. Create a file named test-server.service and paste the following contents.

 1[Unit]
 2Description=test-server service
 3ConditionPathExists=/opt/test-server/test-server
 4
 5[Service]
 6Type=exec
 7User=foo
 8Group=foo
 9
10Restart=on-failure
11RestartSec=5
12
13ExecStart=/opt/test-server/test-server
14
15StandardOutput=journal
16StandardError=journal
17
18[Install]
19WantedBy=multi-user.target

The sections of the systemd service are separated by the section name within square brackets [].

The unit section contains the meta data for the systemd unit. It contains the description of the service and ConditionPathExists is provided with a path which must exist for the service to start successfully. In the above case, it contains the path of the server binary.

The Service section contains the actual information on how to run the service. The Type=exec means that systemd considers the service as started only after the binary has been executed. The user and group specify the user and the group to use to run the service. In this case, the foo user and group created in the previous section are used.

Restart=on-failure means that the service will be automatically restarted if it fails. This will be useful to restart our server after it crashes.

RestartSec is the number of seconds to wait before restarting the service. In this case, the server will be restarted 5 seconds after it crashes.

StandardOutput=journal and StandardError=journal redirects the standard output and error to journald, a logging system which can be used to see the logs of the running service. We will discuss more about how journald works in the upcoming sections.

The Install carries installation information for the unit. It is used by the systemctl enable and disable tool during installation of a unit. The WantedBy is a dependency management mechanism used by systemctl to determine when to enable the service. WantedBy=multi-user.target means that the service will be started in run level 2 multi-user.target. To know more about run levels, I recommend reading https://en.wikipedia.org/wiki/Runlevel

Activating the systemd service

Copy the systemd unit file using the following command

sudo cp test-server.service /etc/systemd/system

Then enable the unit using the following command

sudo systemctl enable test-server 

To ensure that the test-server is enabled, run the following command

systemctl status test-server

The above command will print something similar to

○ test-server.service - test-server service
     Loaded: loaded (/etc/systemd/system/test-server.service; enabled; vendor preset: enabled)
     Active: inactive (dead)

The above output, it confirms that this unit is enabled successfully.

The next step is to start the unit. The following command does this.

sudo systemctl start test-server

Running systemctl status test-server again will print the following ouput which confirms that the service is running successfully.

●  test-server.service - test-server service
     Loaded: loaded (/etc/systemd/system/test-server.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2026-07-07 21:35:03 BST; 6s ago
   Main PID: 97159 (test-server)
      Tasks: 6 (limit: 37395)
     Memory: 1.3M
        CPU: 7ms
     CGroup: /system.slice/test-server.service
             └─97159 /opt/test-server/test-server

Jul 07 21:35:03 eniac systemd[1]: Starting test-server service...
Jul 07 21:35:03 eniac systemd[1]: Started test-server service.

Run

curl localhost:8005 

and confirm whether the server returns the following output

Go is awesome!

Viewing the logs of the service

Now that we have the server started, the next step is to check the logs to see whether it’s running. Linux provides structured logging using journald and the journalctl CLI. Remember that the standard output and standard error have been redirected to journal in the unit file using the StandardOutput=journal and StandardError=journal fields. The following command will tail the logs of the test-service unit

1journalctl -f -u test-server
 1Jul 07 21:45:12 eniac systemd[1]: Stopped test-server service.
 2Jul 07 21:45:12 eniac systemd[1]: Starting test-server service...
 3Jul 07 21:45:12 eniac systemd[1]: Started test-server service.
 4Jul 07 21:45:42 eniac test-server[130011]: 2026/07/07 21:45:42 Shutting down server after 30 seconds
 5Jul 07 21:45:42 eniac systemd[1]: test-server.service: Main process exited, code=exited, status=1/FAILURE
 6Jul 07 21:45:42 eniac systemd[1]: test-server.service: Failed with result 'exit-code'.
 7Jul 07 21:45:47 eniac systemd[1]: test-server.service: Scheduled restart job, restart counter is at 18.
 8Jul 07 21:45:47 eniac systemd[1]: Stopped test-server service.
 9Jul 07 21:45:47 eniac systemd[1]: Starting test-server service...
10Jul 07 21:45:47 eniac systemd[1]: Started test-server service.
11Jul 07 21:46:17 eniac test-server[131858]: 2026/07/07 21:46:17 Shutting down server after 30 seconds
12Jul 07 21:46:17 eniac systemd[1]: test-server.service: Main process exited, code=exited, status=1/FAILURE
13Jul 07 21:46:17 eniac systemd[1]: test-server.service: Failed with result 'exit-code'.
14Jul 07 21:46:23 eniac systemd[1]: test-server.service: Scheduled restart job, restart counter is at 19.
15Jul 07 21:46:23 eniac systemd[1]: Stopped test-server service.
16Jul 07 21:46:23 eniac systemd[1]: Starting test-server service...
17Jul 07 21:46:23 eniac systemd[1]: Started test-server service.

The log line Shutting down server after 30 seconds confirms that the server was shutdown after 30 seconds as expected and after 5 seconds systemd is restarting the server again automatically!.

This auto restart functionality of systemd is useful to restart the server when it panics because of runtime error.

I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter or LinkedIn. Have a good day.

If you are looking for a simple tool to check ssl expiry, visit my website https://sslnotify.com/.