Skip to content

Deploy Rust web application with NGINX and Let's Encrypt

In this article, we will guide you to set up an Axum web application in a production machine with NGINX and SSL support using Let's Encrypt. We are using Debian 10 in this tutorial. Don't worry, we applied the same steps to the Rocket web framework running on CentOS 7 and it works well.

Setting up NGINX

Install and start the NGINX.

debian@remote:~$ # for Debian
debian@remote:~$ sudo apt install -y nginx

centos@remote:~$ # for CentOS
centos@remote:~$ sudo dnf install -y nginx

debian@remote:~$ sudo systemctl enable nginx
debian@remote:~$ sudo systemctl start nginx
NGINX welcome message

If that doesn't work, you need to allow both 80 (HTTP) and 443 (HTTPS) ports using either firewall-cmd or ufw. The HTTPS port will be used later during the Let's Encrypt setup.

Setting Up Server Blocks (Optional)

Now let's create a sample server block.

debian@remote:~$ sudo mkdir -p /var/www/webapp.xyz/html
debian@remote:~$ sudo chown -R $USER:$USER /var/www/webapp.xyz/html

Place the code below in /var/www/webapp.xyz/html/index.html.

<html>
<head>
<title>Welcome to webapp.xyz</title>
</head>
<body>
<h1>Success! Your Nginx server is successfully configured for <em>webapp.xyz</em>. </h1>
<p>This is a sample page.</p>
</body>
</html>

Then add the appropriate configuration in /etc/nginx/conf.d/webapp.xyz.conf

server {
listen 80;
listen [::]:80;

root /var/www/webapp.xyz/html;
index index.html index.htm index.nginx-debian.html;

server_name webapp.xyz www.webapp.xyz;

location / {
try_files $uri $uri/ =404;
}
}

Run sudo nginx -t to check if the configurations are OK. If no errors occurred, restart the NGINX using sudo systemctl restart nginx.

debian@remote:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Warning: If you head to webapp.xyz right now, you will get "403 Forbidden". In the case of CentOS, you need to run the code below

centos@remote:~$ sudo chcon -vR system_u:object_r:httpd_sys_content_t:s0 /var/www/webapp.xyz/

Now you will see "Success! Your NGINX server is successfully configured for webapp.xyz".

Setting Up The Application

Prepare the Rust compiler.

debian@remote:~$ # Debian
debian@remote:~$ sudo apt -y install gcc && curl https://sh.rustup.rs -sSf | sh -s -- -y

centos@remote:~$ # CentOS
centos@remote:~$ sudo dnf -y install gcc && curl https://sh.rustup.rs -sSf | sh -s -- -y

debian@remote:~$ source $HOME/.cargo/env

If you get curl: command not found. You need to install it.

debian@remote:~$ # Debian
debian@remote:~$ sudo apt -y install curl

centos@remote:~$ # CentOS
centos@remote:~$ sudo dnf -y install curl

Create a directory for the application.

debian@remote:~$ sudo mkdir -p /opt/webapp && sudo chown -R $USER: /opt/webapp

Upload your source code to the remote machine.

debian@local:~$ # In local machine! Notice the username in the prompt.
debian@local:~$ cd /code/webapp
debian@local:~$ rsync -avrzP --exclude 'target/' -e "ssh -i ~vm-key.pem" . centos@10.10.10.10:/opt/webapp

If you encounter a similar error below. Make sure rsync is installed on your remote machine.

bash: rsync: command not found
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(228) [sender=3.2.3]

Build the application.

debian@remote:~$ cd /opt/webapp
debian@remote:~$ cargo build --release

debian@remote:~$ # export the environment variable if you had one
debian@remote:~$ source .env

Run the application ~/opt/webapp/target/release/webapp &. Make sure it runs well by hitting its endpoint in your remote machine.

debian@remote:~$  curl 127.0.0.1:3000/health
{"data":{"status":"Running"}}

Make every request to NGINX forwarded to our web application.

  server_name webapp.xyz www.webapp.xyz;

location / {
- try_files $uri $uri/ =404;
+ proxy_pass http://127.0.0.1:3000;
}
}

Make sure everything is OK with sudo nginx -t and restart the NGINX. Check if your web application runs well by doing the request to port 80. You can omit the port, as it is the default.

debian@remote:~$ curl 127.0.0.1:80/health
debian@remote:~$ curl 127.0.0.1/health

If this doesn't work, comment out this line in /etc/nginx/nginx.conf

        include /etc/nginx/conf.d/*.conf;
+ #include /etc/nginx/sites-enabled/*;

Now try to do a request from a local machine, to make sure it works from an external request. Ignore the jq command, it is just there to prettify the JSON output.

debian@remote:~$ curl --location 10.10.10.10/health | jq
{
"data": {
"status": "Running"
}
}

Stop the running application using kill or killall. Now we will use the systemd unit to manage the application. This gives us better control and persistent logging.

Put the code below in /etc/systemd/system/webapp.xyz.service. Change the ExecStart to your binary location.

Warning: Avoid putting comments in systemd unit file, as it causes unexpected behavior.

[Unit]
Description=webapp

[Service]
User=debian
Group=debian
WorkingDirectory=/opt/webapp
Environment="LOG_LEVEL=critical"
ExecStart=/opt/webapp/target/release/webapp

[Install]
WantedBy=multi-user.target

If you are using CentOS, change User and Group to 'centos' (without the quotes). Maximize the use of ROCKET_* If you are using Rocket.

Environment="ROCKET_LOG_LEVEL=critical"

Now you can use systemctl command to manage the application and journalctl to see the logs.

debian@remote:~$ sudo systemctl start webapp.xyz.service
debian@remote:~$ sudo systemctl status webapp.xyz.service
● webapp.xyz.service - webapp
Loaded: loaded (/etc/systemd/system/webapp.xyz.service; disabled; vendor preset: enabled)
Active: active (running) since Thu 2022-08-18 01:57:37 UTC; 1s ago

debian@remote:~$ journalctl -u webapp.xyz.service
Aug 18 01:57:37 user systemd[1]: Started webapp.
Aug 18 01:57:37 user webapp[31351]: listening on 127.0.0.1:3000

Make sure it works the same as manual invocation.

debian@local:~$ curl http://10.10.10.10:80/health
debian@local:~$ curl http://webapp.xyz:80/health

Tips: Sometimes you get unexpected behavior. Don't panic!. You can always inspect the log.

If you get "Bad Gateway" from the curl result. Always consult the NGINX log.

debian@remote:~$ sudo tail -F /var/log/nginx/error.log

In my case, I got "Permission denied".

2021/05/26 00:06:11 [crit] 21856#0: *1 connect() to 0.0.0.0:3000 failed (13: Permission denied) while connecting to upstream, client: 114.125.113.56, server: webapp.xyz, request: "GET /health HTTP/1.1", upstream: "http://0.0.0.0:3000/health", host: "webapp.Turns"

The issue is related to SELinux (CentOS) as described in this answer

centos@remote:~$ sudo setsebool -P httpd_can_network_connect 1

Setting Up The SSL

Install the certbot.

debian@remote:~$ sudo dnf install -y epel-release
debian@remote:~$ sudo dnf install -y certbot python3-certbot-nginx

Run the certbot.

debian@remote:~$ sudo certbot --nginx -d anymid.xyz -d www.anymid.xyz

Test if the SSL works.

debian@remote:~$ curl https://anymid.xyz/health/ # {"data":{"status":"running"}}

Renew SSL Certificate Automatically

It will be a huge pain if we renew the certificate manually all the time. Let the cron handle this.

Add the code below to cron using sudo crontab -e.

0 0,12 * * * python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew --quiet
debian@remote:~$ sudo crontab -e # then paste the code above

Check if the task was inserted successfully.

debian@remote:~$ sudo crontab -l
0 0,12 * * * python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew --quiet

The code above will run in the afternoon and at midnight every day. The random section will select a random minute within the hour for the renewal.

Notes

Forwarding the header from NGINX to the application is different in Axum and Rocket. In Axum, I can get the header out of the box. But in Rocket, I need to add some solutions. I use the following configurations in my Rocket application.

    location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-NginX-Proxy true;
+ proxy_redirect off;
proxy_pass http://0.0.0.0:8000;
}

The initial article is using Rocket and was written a year ago. The improved version is using Axum and is written today. So take the time difference into the account too.


If you liked this article, please support my work. It will definitely be rewarding and motivating. Thanks for the support!

Credits