Posted 1/2/2021.

Deploying a website with zero downtime

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.


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, I would use the following directory structure:

β”œβ”€β”€ current -> /var/www/
└── releases
β”œβ”€β”€ 1609603721
└── 1609604708

Each release is stored separately under /var/www/, and the current release is linked to from /var/www/

Prepare the target server by creating the required directory structure under /var/www/

mkdir -p '/var/www/'

The directory /var/www/ 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/

scp -qr "site" "$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/ 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/

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


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:

  1. Upload a new release as /var/www/DOMAIN/releases/UNIX_TIME.
  2. Create a symbolic link /var/www/DOMAIN/releases/current β†’ /var/www/DOMAIN/releases/UNIX_TIME.
  3. Rename the symbolic link /var/www/DOMAIN/releases/current to /var/www/DOMAIN/current.
  4. Delete old releases.

I have created a small script that can be used from a remote machine:


SOURCE_DIR="${1:?Missing source directory}"
DOMAIN="${2:?Missing domain}"
REMOTE="${3:?Missing target server and user}"
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:

./ 'site' '' ''

No external dependencies are required. Deploying content on a web server has never been easier.