Skip to content

Instantly share code, notes, and snippets.

Last active Feb 25, 2020
What would you like to do?
Deploy bot

Continuous deployment with Git and SSH

This article presents how to deploy continuously from a Git repository with high security, by creating a UNIX user whose only purpose and ability is to update a repository and execute commands from a script within the repository upon successful SSH connections.


Your server has at least Git and some SSH agent installed, and you are connected to it as root.

Just to rephrase: all these commands are to be executed on your server, as root. ssh root@YOUR_SERVER now!

If your server is a blank Debian or Ubuntu and you don't use a configuration management system such as Puppet or Ansible, you probably want to sudo apt-get install git.


These instructions refer to some variables. You can interpret them manually as you go, or define them up front.

You have to set these two variables with values specific to your deployment:

export REPO_NAME=your_repo_name
export  # make sure this is an SSH URL and not an HTTPS URL

I recommend to use the following values for the rest of the commands:

export USERNAME=deploy

Creating the user

Create a user that can only log in through SSH, with no password authentication possible:

adduser $USERNAME --shell $ABSOLUTE_PATH_TO_DEPLOY_SCRIPT --disabled-password --gecos ''
  • The empty gecos option means no interactive prompt for non-applicable details, such as full name, room number…
  • The shell option changes the login shell (the command that is executed when the user logs in) from the default interactive shell (something like bash) to your deployment script.
  • The disabled-password option means this user can only log over SSH.

Now, let's allow that user to log in over SSH:

ssh-keygen -m PEM -f ~/.ssh/${USERNAME} -N ''  # set no passphrase, use PEM and RSA for better compatibility with CI providers (CircleCI needs it for example)
mkdir /home/$USERNAME/.ssh
cat ~/.ssh/${USERNAME}.pub >> /home/$USERNAME/.ssh/authorized_keys

You should probably also add your own and your coworkers' public keys to authorized_keys, in order to mitigate disruption if you ever need to disable that newly created robot key.

Adding a deploy key

We have created an SSH key to log in as the deploy user, but we have not given it a key to pull the code yet. Let's create that key now:

runuser -u $USERNAME -- ssh-keygen -f /home/$USERNAME/.ssh/id_ed25519 -t ed25519 -N ''  # use more secure elliptic curve over RSA

Then, we need to add the deploy key to the list of keys that are allowed to pull our code. You can usually do that through the user interface of your Git hosting provider. For example, with GitHub, you can do that in Settings → Deploy keys. You should add the deploy key by copying the contents of /home/$USERNAME/.ssh/

For maximum security, do not give push access to that key, only pull access.

Adding the deployment script to your repository

Your deployment script could be stored in scripts/ in your repository and look something like the example file given in this gist, concatenated with some language and deployment-specific elements. Make sure to chmod u+x scripts/, as it is this file that will be executed upon deployment! This technique also means your deployment script will auto-update.

For an NPM-based stack, check out, for example.

The following instructions assume you have pushed your deployment script to your master branch. If you want to work on a temporary cd branch to try them out, make sure you git checkout cd on the server and change the TARGET_BRANCH in the deploy script, otherwise each deployment will force the change to master.

Cloning the repository

Let's clone the repository:

cd /home/$USERNAME
runuser -u $USERNAME -- git clone $REPO_URL

You can now try your deployment with su $USERNAME: you should not be prompted for any login information, but that command should trigger a pull.

If that works, congratulations, you're all set! You should double-check that your SSH login works by ssh $USERNAME@$SERVER from your machine.

Deploying from your CI

One of the great advantages of this method is that it allows for fine-grained access control. You can create one SSH key per allowed server, and revoke them individually, simply by adding and removing from .ssh/authorized_keys.

The first thing you should do, though is add a private deploy key to your CI environment. You can get the first one we created from /home/root/.ssh/${USERNAME}.

You then need to add the deployment step to your CI config, which really means simply making an SSH connection to deploy@your_server.

For example, with CircleCI:

      - image: circleci/node:9.9

      - add_ssh_keys:
            - $(ssh-keygen -E md5 -lf $
      - run: ssh-keyscan -H $YOUR_SERVER >> ~/.ssh/known_hosts



Safely using sudo in the deploy script

If you need to update something like the Nginx config in your deployment script that then requires using sudo (such as sudo service nginx reload), the safe way to do this is to whitelist only these commands so that deploy can execute them without needing actual superuser rights.

In order to do this, add the following file in /etc/sudoers.d/deploy:

# Allow deploy user to reload and restart Nginx
deploy ALL=NOPASSWD: /usr/sbin/service nginx reload, /usr/sbin/service nginx restart

You can then add the following in your

### Update NGinx conf
# This only gives the deploy user the right to edit their site in `sites-available`, never `sites-enabled`: only the root user should be able to edit those, with `ln -s /etc/nginx/sites-available/$REPO_NAME /etc/nginx/sites-enabled
cp conf/nginx /etc/nginx/sites-available/$REPO_NAME  # this assumes the `deploy` user has write rights on this file (not the case by default, give them with `touch` and `chown`)
sudo /usr/sbin/service nginx reload  # this assumes the `deploy` user has rights to `sudo` at least for reloading the Nginx config (not the case by default)
# Install dependencies
npm ci --production
# Start server
npm stop || echo 'Server was not started, continuing'
npm start
set -e # exit immediately on any failing command
### Deployment info only
# To get info about the current state, connect with `ssh deploy@server current`
if [[ $2 == 'current' ]] # first arg is always -c, passed by `ssh`
git log --pretty=oneline HEAD -n 1
set -x # log all commands
# Check out code
# If you used a different SSH key name than `id_$algorithm`, uncomment the following lines to import it manually.
# eval `ssh-agent`
# ssh-add /home/deploy/.ssh/deploy-bot
# One does not simply `git pull`: we need to clean all leftover files and support force pushes
git fetch origin $TARGET_BRANCH # get the remote code, do not try to apply it just yet
git checkout --force --detach origin/$TARGET_BRANCH # ignore divergences, always trust remote
git clean --force # remove all leftover files in tracked folders, prevent conflicts with previously ignored files
# Install dependencies and start server.
# Depends on your stack and setup!
# Example scripts for different languages given in this repo.

This comment has been minimized.

Copy link

@cbenz cbenz commented Apr 5, 2017

I recommend using adduser --shell option instead of usermod -s.

adduser $USERNAME --shell $ABSOLUTE_PATH_TO_DEPLOY_SCRIPT --disabled-password --gecos ''
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment