Over the last few years, I have built many websites and web-based applications that I have had to deploy on various Linux servers. This post presents an easy deployment method that I have found to work in most situations.
The method works with both Apache and Nginx. It can be used to deploy content to any Linux distribution.
Prerequisites
- An SSH server such as OpenSSH running on the target server.
- An SSH client installed on the local machine.
- An SSH-key pair that can be used to connect to the remote SSH server from the local machine.
- A web server such as Apache or Nginx installed on the target machine.
This guide describes a setup with Nginx running on Ubuntu 20.04.
Prepare the target server
I like to keep a separate directory, under /var/www
, for each domain or service that is served from every single server. This is flexible yet simple to maintain.
For the domain example.com
, I would use the following directory structure:
/var/www/example.com
├── current -> /var/www/example.com/releases/1609604708
└── releases
├── 1609603721
└── 1609604708
Each release is stored separately under /var/www/example.com/releases
, and the current release is linked to from /var/www/example.com/current
.
Prepare the target server by creating the required directory structure under /var/www/example.com
:
mkdir -p '/var/www/example.com/releases'
The directory /var/www/example.com
will henceforth be denoted as $ROOT
in commands.
Upload a release
All releases must be given a unique name. A simple method is to use Unix time of the release. This can be done through the command date +%s
(denoted as $RELEASE
in future commands).
Upload the source directory site
to the target server under a new directory /var/www/example.com/releases/RELEASE
:
scp -qr "site" "root@example.com:$ROOT/releases/$RELEASE"
Serve the new release
Now comes the tricky part: replacing the existing site content without downtime. In order to do this, the link /var/www/example.com/current
must be changed to point to the newly uploaded release.
The symbolic link can be atomically changed by first creating the link and then moving it so that it overwrites /var/www/example.com/current
:
ln -s "$ROOT/releases/RELEASE" "$ROOT/releases/current"
mv -Tf "$ROOT/releases/current" "$ROOT/current"
See Artem Chistyakov's post "Atomic symlinks" for a comprehensive guide on how a symbolic link can be moved in a single instruction.
Clean up old releases
The release directory on the target server will fill up over time. It is therefore useful to delete old releases.
I often keep the five latest releases in case a rollback is required. Realistically, though, I have never had to rollback. Instead, I prefer to roll-forward at all times, making a new release with old content if necessary.
The following command can be executed on the target server in order to delete all but the five latest releases:
cd "$ROOT/releases" && \
ls | sort -r | tail -n +6 | xargs -d '\n' -r rm -rf --
Summary
Set up a web server to serve content from /var/www/DOMAIN/releases/current
(or a link pointing to it).
Deploy content to ut using the following method:
- Upload a new release as
/var/www/DOMAIN/releases/UNIX_TIME
. - Create a symbolic link
/var/www/DOMAIN/releases/current
→/var/www/DOMAIN/releases/UNIX_TIME
. - Rename the symbolic link
/var/www/DOMAIN/releases/current
to/var/www/DOMAIN/current
. - Delete old releases.
I have created a small script that can be used from a remote machine:
#!/usr/bin/bash
# deploy.sh
SOURCE_DIR="${1:?Missing source directory}"
DOMAIN="${2:?Missing domain}"
REMOTE="${3:?Missing target server and user}"
ROOT="/var/www/$DOMAIN"
RELEASE=$(date +%s)
# Ensure that the release directory exists.
ssh "$REMOTE" "mkdir -p '$ROOT/releases'"
# Upload the source directory to the target server.
scp -qr "$SOURCE_DIR" "$REMOTE:$ROOT/releases/$RELEASE"
# Atomically mark the release as current.
ssh "$REMOTE" "
ln -s '$ROOT/releases/$RELEASE' '$ROOT/releases/current'
mv -Tf '$ROOT/releases/current' '$ROOT/current'
"
# Remove old releases.
ssh "$REMOTE" "
cd '$ROOT/releases' && \
ls | sort -r | tail -n +6 | xargs -d '\n' -r rm -rf --
"
The script can be executed like this:
./deploy.sh 'site' 'example.com' 'root@example.com'
No external dependencies are required. Deploying content on a web server has never been easier.