Linux comes with all the basic tools necessary to deploy an application to development and production environments, and to roll back to any past version if something goes wrong. It takes just a few commands to set everything up.

Creating Application User

It’s a good idea to create a user with limited privileges to deploy and run your application. Typically, your main user account has root-level access via sudo, which poses a security risk if your application is compromised.

The following command will create a user named ‘app’ with no password, requiring an SSH key pair to log in for increased security:

me@server:~$ sudo adduser --disabled-password --gecos '' app
Adding user `app' ...
Adding new group `app' (1001) ...
Adding new user `app' (1001) with group `app' ...
Creating home directory `/home/app' ...
Copying files from `/etc/skel' ...

We also need to ensure that the services started by ‘app’ continue to run after logout:

me@server:~$ sudo loginctl enable-linger app

We can now switch to our newly created account and generate an SSH key pair. It’s fine to press Enter to accept the default settings. We will use this key pair exclusively to pull code from our application’s Git repository:

me@server:~$ sudo su - app
app@server:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/app/.ssh/id_rsa): 
Created directory '/home/app/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/app/.ssh/id_rsa
Your public key has been saved in /home/app/.ssh/id_rsa.pub
...

To view the contents of the public key file, run the following command and copy its output to the online Git repository of your choice, such as GitHub or GitLab, as a deploy key (normally found under Settings):

app@server:~$ cat ~/.ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAA... app@server

Once the deploy key is set up, we can clone our application’s repository with:

app@server:~$ git clone [email protected]:me/my-app.git

Before we continue, it’s important to add the SSH public key used for accessing our regular account to the ‘app’ account’s authorized_keys file, enabling direct SSH access to ‘app’.

app@server:~$ nano ~/.ssh/authorized_keys

From this point on, we won’t be needing our regular user account anymore. The rest of the commands must be run after SSH’ing to ‘app’.

Deploy Script

Since we have our application code in place, it’s time to create a deploy script. Here’s one for a typical Node.js application:

app@server:~$ nano deploy
#!/bin/bash

APP_REPO='/home/app/my-app'
APP_DEPLOYMENTS='/home/app/deployments'

# Exit immediately if a command fails
set -e

# Pull changes
cd $APP_REPO
git reset --hard
git pull

# Install dependencies and run tests
npm install
npm run test

# Create new deployment directory and copy the code over
NOW=$(date --iso-8601=minutes | sed 's/:/-/g' | cut -c 1-16)
mkdir -p $APP_DEPLOYMENTS
cd $APP_DEPLOYMENTS
cp -a $APP_REPO $NOW

# Point to new deployment using a symbolic link
ln -vfns $NOW current

# Restart app
systemctl --user restart my-app

We make the deploy script executable with:

app@server:~$ chmod +x deploy

In the deploy script, we pull changes, install dependencies, run tests, and create a new deployment directory named with the current date and time, such as 2024-05-19T16-42. Finally, we update the symbolic link named current to point to the new deployment directory and then restart the application.

After a few executions of our deploy script, the deployments directory should look like the following:

app@server:~$ ~/deploy
...
app@server:~$ ls -l ~/deployments/
drwxrwxr-x 4 app app 4096 May 18 09:53 2024-05-18T09-53
drwxrwxr-x 4 app app 4096 May 19 13:25 2024-05-19T13-25
drwxrwxr-x 4 app app 4096 May 19 16:42 2024-05-19T16-42
lrwxrwxrwx 1 app app   16 May 19 16:42 current -> 2024-05-19T16-42

If anything goes wrong with the deploy script, such as failing tests, no harm will be done because the script exits upon the first error encountered.

Rolling Back Deployments

To roll back to a previous deployment, just two commands are needed:

  1. Update the symbolic link to point to a different deployment directory.
  2. Restart the application.
app@server:~$ ln -vfns ~/deployments/2024-05-19T13-25 ~/deployments/current
app@server:~$ systemctl --user restart my-app

Service File

In the deploy script and the rollback example above, we restart our application by using systemctl, a widely used service manager in Linux. However, we haven’t created a service file for our application yet, so let’s do that:

app@server:~$ mkdir -p  ~/.config/systemd/user/
app@server:~$ nano ~/.config/systemd/user/my-app.service
[Unit]
Description=My App

[Service]
WorkingDirectory=/home/app/deployments/current
Environment=DB_HOST='localhost'
Environment=DB_PORT=5432
Environment=DB_USERNAME='my_app'
Environment=DB_PASSWORD='...'
ExecStart=/home/app/.nvm/versions/node/v20.13.1/bin/node server.js
RestartSec=30
Restart=always

[Install]
WantedBy=multi-user.target

The service file is mostly self explanatory. We define the working directory of our application, any environment variables the application needs, the command to start the application, and the application restart behavior in case of a crash.

The WantedBy=multi-user.target line ensures that our service is launched only after the server can connect to the network in the event of a server restart.

For a complete list of options, you might want to take a look at Systemd service unit configuration.

Finally, we can enable and start our service with the following commands:

app@server:~$ systemctl --user enable my-app
app@server:~$ systemctl --user start my-app

The status of the service can be checked with:

app@server:~$ systemctl --user status my-app
● my-app.service - My App
     Loaded: loaded (/home/app/.config/systemd/user/my-app.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2024-05-19 09:27:08 UTC; 4s ago
   Main PID: 1582880 (node)
     CGroup: /user.slice/user-1001.slice/[email protected]/my-app.service
             └─1582880 /home/app/.nvm/versions/node/v20.13.1/bin/node server.js

May 19 09:27:08 server systemd[1563720]: Started My App.
May 19 09:27:08 server node[1582880]: Server is running on http://localhost:8000

We can review the logs our application generates with journalctl. For example, the following command shows the last 100 entries:

app@server:~$ journalctl --user -n100 -u my-app

Bonus: Putting our Application Behind a Web Server

Our application is now up and running on http://localhost:8000, but it lacks HTTPS support and features such as HTTP access logs. We can set up a web server as a reverse proxy to redirect requests to internal ports.

Let’s switch to our main user account, install the nginx web server, and create a configuration for our application:

me@server:~$ sudo apt install nginx
me@server:~$ sudo nano /etc/nginx/sites-available/default

The following configuration redirects all HTTPS requests to our application. HTTP requests are automatically redirected to HTTPS as well. If you are using a DNS provider like CloudFlare, you can use the SSL certificate files they provide, or you can use a free SSL certificate service like Let’s Encrypt’s Certbot.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;
    ssl_certificate     /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/certs/example.com.key;

    location / {
        proxy_pass          http://localhost:8000/;
        proxy_set_header    Host $host;
        proxy_set_header    X-Real-IP $remote_addr;
    }
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

After we’ve saved the configuration, it’s a good idea to check its integrity with nginx configtest. If all is well, we can then tell nginx to reload its configuration:

me@server:~$ sudo service nginx configtest 
 * Testing nginx configuration                                                     [ OK ] 
me@server:~$ sudo service nginx reload

Web server access and error logs can be found in /var/log/nginx.

Wrap up

We’ve made application deployments and rollbacks possible with nothing more than the built-in Linux tools. One can easily create multiple environments for development, staging, and production. All it takes is a deploy script, a service file, and a few commands run over SSH.


Related: