Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
My fabfile for deploying a Flask app on a Digital Ocean droplet
from fabric import task
import os
import getpass
import subprocess
# you also need to install doctl and set it up for create/destroy to work
APP_NAME = 'the-name-of-your-app'
DOMAIN = '' # SSL is not in here yet
USER = 'your-username'
PUB_KEY = '~/.ssh/'
PUB_KEY_MD5 = 'your-public-key-md5'
NGINX_USER = 'www-data'
NGINX_USER_GROUP = 'www-data'
def create(c):
create_droplet = ['doctl', 'compute', 'droplet', 'create']
droplet_name = input("Enter a droplet name: ")
create_droplet += [droplet_name]
create_droplet += ['--size', 's-1vcpu-1gb']
create_droplet += ['--image', 'ubuntu-18-04-x64']
create_droplet += ['--region', 'nyc1']
create_droplet += ['--ssh-keys', PUB_KEY_MD5]
create_droplet += ['--region', 'nyc1']
create_droplet += ['--wait']
create_droplet += ['--format', 'ID,Name,PublicIPv4']
create_droplet += ['--no-header']
p =, stdout=subprocess.PIPE)
droplet_info = p.stdout.decode('utf-8')
print("Created a new droplet!")
def destroy(c):
prompt = "Enter the number of a droplet to destroy it: "
droplet_to_destroy, droplets = select_droplet(prompt)
if (droplet_to_destroy is None or
droplet_to_destroy < 0 or
droplet_to_destroy >= len(droplets)):
print("Invalid droplet selected")
return 1
drop_id, drop_name, drop_ip = droplets[droplet_to_destroy].split()
delete_droplet = ['doctl', 'compute', 'droplet', 'delete']
delete_droplet += [str(drop_id)]
p =
def select_droplet(prompt="Select a droplet: "):
list_droplets = ['doctl', 'compute', 'droplet', 'list']
list_droplets += ['--format', 'ID,Name,PublicIPv4']
list_droplets += ['--no-header']
p =, stdout=subprocess.PIPE)
droplets = p.stdout.decode('utf-8').split('\n')[:-1]
for i, droplet in enumerate(droplets):
print(i, ":\t", droplet)
selected = int(input(prompt))
except ValueError:
selected = None
return selected, droplets
def init(c):
# install python3-pip python3-dev nginx'apt-get update')'apt-get install python3-pip python3-dev nginx -y')
# create the working directory for the app'pip3 install virtualenv')'mkdir -p /var/www/{}'.format(APP_NAME))
# set up bare git repo separate from the work directory'mkdir -p /var/repo/{}.git'.format(APP_NAME))'git init --bare /var/repo/{}.git'.format(APP_NAME))
# set up post-receive hook for copying files to the work tree
with'/var/repo/{}.git/hooks'.format(APP_NAME)):'touch post-receive')'chmod +x post-receive')
# check out the files after a push'echo "#!/bin/sh" >> post-receive')'echo "git --work-tree=/var/www/{0} '
'--git-dir=/var/repo/{0}.git checkout -f" '
'>> post-receive').format(APP_NAME))
# create app virtual environment'virtualenv /var/www/{0}/{0}_env'.format(APP_NAME))
# create a directory for the gunicorn socket(s) and database(s)'mkdir -p /run/gunicorn')'chown root:{} /run/gunicorn'.format(NGINX_USER_GROUP))'chmod 770 /run/gunicorn')'chmod g+s /run/gunicorn')
# set up UFW to allow nginx and OpenSSH'ufw allow OpenSSH')'ufw allow "Nginx Full"')'ufw enable')'ufw status')
# TODO: set up domain records
# TODO: set up SSL with Let's Encrypt
# push up the code
update(c, first_push=True,
# initialize the database
def adduser(c, username=USER, pubkey_file=PUB_KEY):
# create a new user and make it a sudoer
new_pass = getpass.getpass("Enter a password for the new user")'adduser --disabled-password --gecos "" {}'.format(username))'usermod -aG sudo {}'.format(username))'echo "{}:{}" | chpasswd'.format(username, new_pass))
# add public key
with open(os.path.expanduser(pubkey_file)) as fd:
ssh_key = fd.readline().strip()'mkdir -p -m 700 /home/{}/.ssh'.format(username))'chown {0}:{0} /home/{0}/.ssh'.format(username))'touch /home/{}/.ssh/authorized_keys'.format(username))'echo "{}" >> /home/{}/.ssh/authorized_keys'.format(ssh_key, username))'chown {0}:{0} /home/{0}/.ssh/authorized_keys'.format(username))'chmod 600 /home/{}/.ssh/authorized_keys'.format(username))
def update(c, first_push=False, stop_server=True, start_server=True):
if stop_server:
remote_name = input("Enter a name for the git remote destination [live]: ") or 'live'
if first_push:
# set remote for local git with server name
c.local('git remote add {} '.format(remote_name) +
c.local('git push --set-upstream {} master'.format(remote_name))
c.local('git push {}'.format(remote_name))
# update venv'./{}_env/bin/pip install -r requirements.txt'.format(APP_NAME))
# link <app-name>.nginx to sites-available'ln -f -s /var/www/{0}/{0}.nginx /etc/nginx/sites-available/{0}'.format(APP_NAME))
# enable systemd service'systemctl -f enable /var/www/{0}/{0}.service'.format(APP_NAME))
# add any cron job(s) here'crontab -u {0} /var/www/{1}/{1}.crontab'.format(NGINX_USER, APP_NAME))
if start_server:
def start(c):
# start gunicorn'systemctl start {}'.format(APP_NAME))
# remove default from sites-enabled'rm -f /etc/nginx/sites-enabled/default')
# link nginx conf file to sites-enabled'ln -s /etc/nginx/sites-available/{0} /etc/nginx/sites-enabled/{0}'.format(APP_NAME))
# restart nginx'systemctl restart nginx')
def stop(c):
# stop gunicorn'systemctl stop {}'.format(APP_NAME))
# unlink nginx conf file to sites-enabled'rm -f /etc/nginx/sites-enabled/{}'.format(APP_NAME))
# restart nginx'systemctl restart nginx')
def db_init(c):
# check if DB already exists, request db_kill if you really want to
# overwrite it
with'/var/www/{}'.format(APP_NAME)), c.prefix('export DB_BASE_DIR="/run/gunicorn"'):'source {0}_env/bin/activate; echo -e "from {0} import db\ndb.create_all()" | python'.format(APP_NAME))
def db_kill(c):
print("YOU ARE ABOUT TO DELETE THIS DATABASE FILE ON THE SERVER:")'ls -l /run/gunicorn/{}.db'.format(APP_NAME))
kill = input("ARE YOU SURE? [y/N]: ")
if kill == 'y':'rm -f /run/gunicorn/{}.db'.format(APP_NAME))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.