HOME
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.
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
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.
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
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".
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.
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
{{ callout(style="tip", 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_connect 1
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"}}
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.
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!
Comments