What??

Updated: 2020-06-24

We are going to make a demo linux web-server with systemd , config file and installable .deb binary in rust. I have been creating web-servers in Node.js for a while. When it comes to running it in production almost always I end up using a solution like pm2 or nodemon to keep the service up and use nginx or apache as reverse proxy to that service. This tutorial is written for people who are fairly new to rust and linux. If you are a developer with battle scars you might get a little out of it ๐Ÿ˜‰ so please bear with me. You can also look at the repo here .

Background

Since I started using rust for most of my new projects, I had this itch to figure out creating an installable binary with systemd support (that takes care of running the service as a daemon) and a config file that I can edit without changing any code. If and only if I configure my server correctly I don’t need to run a reverse proxy either (I’m not going to cover this in here).

Rust ecosystem is growing at a very rapid pace, there are so many cool tools out there now that make the following process very approachable. While searching I stumbled upon cargo-deb an amazing tool to create .deb files with just one command. More on that later..

I have worked with actix-web and rocket regularly but for this time I just wanted to try out tide ๐ŸŒŠ , please go ahead an read up on the readme file for more details about what tide is. We will use config crate to easily parse config files into rust values. This might be an overkill here but hey this is not a real app. For the demo I will only be using the config file to get server_address and server_port values. You can use the same concept to load any variables from the config.

crates we are going to use

  • tide // WIP modular web framework
  • config // Layered configuration system for Rust applications
  • serde // A generic serialization/deserialization framework
  • serde_derive // Macros 1.1 implementation of #[derive(Serialize, Deserialize)]

Prerequisite

Let’s go ahead and create a new cargo project with running cargo new tide-server && cd tide-server in your terminal. We need to use rust nightly because tide requires nightly. If you are using rustup as you should, simply run rustup override set nightly in the project folder. We also need to install cargo-deb, to do so run cargo install --force cargo-deb and we are good to go.

Start Coding

Open project in your editor and add the following dependencies to your cargo.toml file

[dependencies]
tide = "0.9"
config = "0.10"
async-std = "1.6"
serde = {version = "1.0", features = ["derive"]}

Then we create a settings.rs file in src/ folder and add the following.

use config::{Config, ConfigError, File};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Settings {
    pub server_address: String,
    pub server_port: u16,
}

impl Settings {
    pub fn new() -> Result<Self, ConfigError> {
        let builder = Config::builder();

        // read the local file onyl in development mode
        #[cfg(debug_assertions)]
        let builder = builder.add_source(File::with_name("assets/tide-config.ini"));

        #[cfg(not(debug_assertions))]
        let builder = builder.add_source(File::with_name("/usr/local/etc/tide-config.ini"));

        let config = builder.build()?;
        config.try_deserialize()
    }
}

We create a Settings struct and leverage config crate to populate the values in it by reading the file. As you have noticed in the impl block we create a new Config and then merge use merge method twice to get values from .ini files. The cfg attribute #[cfg(debug_assertions)] allows us to conditionally compile our code, firs merge only works in development mode and for production the app will need a file in "/usr/local/etc/tide-config.ini" location.

While we are at it let’s create folder assets/ in project root and add a file tide-config.ini with following variables.

server_address = "0.0.0.0"
server_port = "8000"

That’s it we are done with reading from config, we can create setting anywhere in our project by calling the Settings::new(); method it will either give us the struct or an error if the files don’t exist or they don’t have the required variables set in them.

Change the main.rs file like the following.

use async_std::task;
mod settings;

fn main() -> Result<(), std::io::Error> {
    task::block_on(async {
        let settings = crate::settings::Settings::new().unwrap();

        let server_address = format!("{}:{}", settings.server_address, settings.server_port);

        let mut app = tide::new();

        app.at("/").get(move |_| async move { Ok("Hello World!") });

        app.listen(server_address).await?;

        Ok(())
    })
}

We use async_await nightly stable features, which is only required because we are using tide. If you switch to any other web-server that runs on stable, its not needed. Anyhow let’s just walk through what’s going on here. In our main function we create our settings variable, return error if settings can’t be created for any reason. Your program will exit with that error message. Rest is pretty straight forward we onyl have one route and that returns Hello World. You can test this by running curl http://localhost:8000 in another terminal it should return hello world.

Building a .deb file

You could simply run cargo deb in terminal and have a target/debian/tide-server_0.1.0_amd64.deb created for you. You can install this in any debian based linux system and run your server from the command line. But it will fail to run as the config file is won’t be installed. To remedy this we need to add some configuration in cargo.toml file. Make sure your cargo.toml looks like

[package]
name = "tide-server"
version = "0.2.0"
authors = ["mygnu <tech@hgill.io>"]
readme = "README.md"
license = "MPL 2.0"
edition = "2018"

[dependencies]
tide = "0.16"
config = "0.13"
async-std = "1.12"
serde = {version = "1.0", features = ["derive"]}


[profile.release]
lto = true
opt-level = 3

[package.metadata.deb]
maintainer = "Harry Gill <tech@gill.net.in>"
copyright = "2019, Harry Gill"
depends = "$auto, systemd"
conf-files = ["/usr/local/etc/tide-config.ini", "/etc/systemd/system/tide-server.service"]
extended-description = """\
web-server written in rust.\
"""
section = "admin"
priority = "optional"
assets = [
    ["target/release/tide-server", "/usr/local/bin/", "755"],
    ["assets/tide-config.ini", "/usr/local/etc/", "644"],
    ["assets/tide-server.service", "/etc/systemd/system/", "644"],
]

We add a licence in this case its MPL 2.0. And [package.metadata.deb] part is read by the cargo deb command before it produces the binary. I’ll explain some of the stuff here. depends tells the system that this binary requires systemd to be present and $auto would try to determine if there are any other dependencies to consider. conf-files is where we tell the binary to not overwrite when re-installing the app. assets are the files that will be copied to the target dir on the system when we install the app. The actual binary goes to /usr/local/bin, config file goes to /usr/local/etc/ and systemd file goes to /etc/systemd/system. We also define what permission those files will have when they are installed.

Systemd Unit file

We want our server to start and restart automatically like a real linux app ๐Ÿ˜‰. To do so we will be using systemd file, go ahead and create assets/tide-server.service and fill in with following content.

[Unit]
Description=rust server
After=network.target

[Service]
ExecStart=/usr/local/bin/tide-server
Type=exec
Restart=on-failure

[Install]
WantedBy=default.target

This is called a unit file you can read up more about this here . In a nutshell it tells systemd that we want to execute our binary after the network is available, and restart automatically if it fails.

Installing and Testing

Run cargo deb in the terminal and voila you have successfully made a linux app installer in rust. If you are running a distro that has gnome-sofware on it you can simply double click and install through the GUI or do sudo dpkg -i target/debian/tide-server_0.1.0_amd64.deb from terminal. Now lets confirm that our files are installed properly, please run the following in terminal.

cat /usr/local/etc/tide-config.ini
server_address = "0.0.0.0"
server_port = "8000"
ls -la /usr/local/bin/tide-server
-rwxr-xr-x 1 root root 2280000 Jan 30 12:40 /usr/local/bin/tide-server

Stop any development server if it is running on the same port. Now you can start the server with sudo systemctl start tide-server and check status sudo systemctl status tide-server if everything is good you should see a line with Server is listening on: http://0.0.0.0:8000 . Try curl http://localhost:8000 again and see if you get the Hello from the server. Go ahead and change the config file, maybe the port, and sudo systemctl restart tide-server.

Congratulation!! now you can distribute/deploy linux app made with rust.

If you enjoyed reading this post, you can click ๐Ÿ‘ at the top a few times.
Please get in touch with me on https://hachyderm.io/@mygnu (Mastodon) if you have a question or suggestions.

Thank you for reading and Happy Coding!