Setting Up Rust Web App with NGINX

Deploy Rust web application with NGINX and Let's Encrypt

Table of contents

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.
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

</svg><p class="callout__text">

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:~$# Debiandebian@remote:~$sudo apt -y install gcc&&curlhttps://sh.rustup.rs -sSf|sh-s -- -ycentos@remote:~$# CentOScentos@remote:~$sudo dnf -y install gcc&&curlhttps://sh.rustup.rs -sSf|sh-s -- -ydebian@remote:~$source$HOME/.cargo/env

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

debian@remote:~$# Debiandebian@remote:~$sudo apt -y install curlcentos@remote:~$# CentOScentos@remote:~$sudo dnf -y install curl

Create a directory for the application.

debian@remote:~$sudo mkdir -p /opt/webapp&&sudochown -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/webappdebian@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 foundrsync:connection unexpectedly closed(0bytes received so far)[sender]rsyncerror: error in rsync protocol data stream(code12) at io.c(228)[sender=3.2.3]

Build the application.

debian@remote:~$cd /opt/webappdebian@remote:~$cargo build --releasedebian@remote:~$# export the environment variable if you had onedebian@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/healthdebian@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.

</svg><p class="callout__text">

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

[Unit]Description=webapp[Service]User=debianGroup=debianWorkingDirectory=/opt/webappEnvironment="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.servicedebian@remote:~$sudo systemctl status webapp.xyz.service●webapp.xyz.service - webappLoaded:loaded(/etc/systemd/system/webapp.xyz.service;disabled;vendorpreset: enabled)Active:active(running) since Thu 2022-08-18 01:57:37 UTC;1sagodebian@remote:~$journalctl -u webapp.xyz.serviceAug1801:57:37 user systemd[1]: Started webapp.Aug1801: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/healthdebian@local:~$curl http://webapp.xyz:80/health

</svg><p class="callout__text">

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_connect1

Setting Up The SSL

Install the certbot.

debian@remote:~$sudo dnf install -y epel-releasedebian@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.

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

Check if the task was inserted successfully.

debian@remote:~$sudo crontab -l00,12* * *python3 -c 'import random; import time; time.sleep(random.random() * 3600)'&&certbotrenew --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!

</svg>

Credits

Comments