├── .gitignore ├── .provisioning ├── deploy.yml └── roles │ ├── common │ ├── tasks │ │ └── main.yml │ └── vars │ │ └── main.yml │ ├── gunicorn │ ├── tasks │ │ └── main.yml │ └── templates │ │ ├── gunicorn.conf.j2 │ │ └── supervisor.conf.j2 │ ├── nginx │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ ├── templates │ │ ├── site.conf.j2 │ │ └── site_ssl.conf.j2 │ └── vars │ │ └── main.yml │ └── python │ ├── tasks │ └── main.yml │ └── vars │ └── main.yml ├── README.md ├── Vagrantfile └── demo-flask-app ├── public ├── __init__.py └── hello.py ├── requirements.txt └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | venv/ 3 | demo-flask-app/.idea/ 4 | demo-flask-app/__pycache__/ 5 | demo-flask-app/public/__pycache__/ 6 | -------------------------------------------------------------------------------- /.provisioning/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Configuration for Production 4 | hosts: all 5 | become: yes 6 | vars: 7 | user_name: "vagrant" 8 | group_name: "www-data" 9 | 10 | roles: 11 | - common 12 | - python 13 | - { role: gunicorn, autostart: true, enabled: true } 14 | - { role: nginx, use_ssl: false, enabled: true } -------------------------------------------------------------------------------- /.provisioning/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Ensure bash, OpenSSl, and libssl are the latest versions 4 | apt: 5 | name: 6 | - bash 7 | - openssl 8 | - libssl-dev 9 | - libssl-doc 10 | update_cache: yes 11 | state: latest 12 | 13 | - name: Install common server packages 14 | apt: 15 | name: 16 | - build-essential 17 | - htop 18 | - vim 19 | - git 20 | - unzip 21 | - curl 22 | state: present 23 | -------------------------------------------------------------------------------- /.provisioning/roles/common/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # project name 4 | project_name: demo-flask-app 5 | 6 | # home path for project and virtualenv, e.g. /home/ubuntu 7 | home_path: /{{ user_name }} 8 | 9 | # project path, e.g. /home/ubuntu/fvang 10 | project_path: "{{ home_path }}/{{ project_name }}" 11 | 12 | # flask app path, e.g. /home/ubuntu/fvang/fvang 13 | application_path: "{{ project_path }}" -------------------------------------------------------------------------------- /.provisioning/roles/gunicorn/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install Supervisor 4 | apt: 5 | name: 6 | - supervisor 7 | state: present 8 | 9 | - name: Create the Gunicorn config directory 10 | file: path=/etc/gunicorn state=directory owner={{ user_name }} group={{ group_name }} mode=0700 11 | 12 | - name: Create the Gunicorn config file in /etc/gunicorn/ 13 | template: src=gunicorn.conf.j2 dest=/etc/gunicorn/gunicorn.conf 14 | 15 | - name: Create the Gunicorn log directory 16 | file: path=/var/log/gunicorn state=directory owner={{ user_name }} group={{ group_name }} mode=0700 17 | 18 | - name: Create the Supervisor config file for Gunicorn 19 | template: src=supervisor.conf.j2 dest=/etc/supervisor/conf.d/gunicorn.conf 20 | 21 | - name: Re-read the Supervisor config files 22 | supervisorctl: name=gunicorn state=present 23 | 24 | - name: Start Gunicorn with supervisord 25 | supervisorctl: name=gunicorn state=restarted 26 | when: enabled 27 | 28 | - name: Stop Gunicorn for local dev 29 | supervisorctl: name=gunicorn state=stopped 30 | when: not enabled 31 | -------------------------------------------------------------------------------- /.provisioning/roles/gunicorn/templates/gunicorn.conf.j2: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | workers = multiprocessing.cpu_count() * 2 + 1 4 | proc_name = 'gunicorn' 5 | bind = "127.0.0.1:8000" 6 | errorlog = '/var/log/gunicorn/gunicorn-error.log' 7 | accesslog = '/var/log/gunicorn/gunicorn-access.log' 8 | loglevel = 'warning' 9 | timeout = 60 -------------------------------------------------------------------------------- /.provisioning/roles/gunicorn/templates/supervisor.conf.j2: -------------------------------------------------------------------------------- 1 | [program:gunicorn] 2 | command={{ virtualenv_path }}/bin/gunicorn wsgi:app -c /etc/gunicorn/gunicorn.conf --pythonpath {{ application_path }} 3 | directory={{ application_path }} 4 | environment=PATH="{{ virtualenv_path }}/bin" 5 | user={{ user_name }} 6 | group={{ group_name }} 7 | autorestart=true 8 | autostart={{ autostart | bool | lower }} 9 | redirect_stderr=true -------------------------------------------------------------------------------- /.provisioning/roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Reload Nginx 4 | service: name=nginx state=reloaded 5 | 6 | - name: Stop Nginx 7 | service: name=nginx state=stopped -------------------------------------------------------------------------------- /.provisioning/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install Nginx 4 | apt: 5 | name: 6 | - nginx 7 | state: present 8 | 9 | - name: Create the Nginx configuration file for SSL 10 | template: src=site_ssl.conf.j2 dest=/etc/nginx/sites-available/{{ project_name }} 11 | when: use_ssl 12 | 13 | - name: Create the Nginx configuration file (non-SSL) 14 | template: src=site.conf.j2 dest=/etc/nginx/sites-available/{{ project_name }} 15 | when: not use_ssl 16 | 17 | - name: Ensure that the default site is removed 18 | file: path=/etc/nginx/sites-enabled/default state=absent 19 | 20 | - name: Ensure that the application site is enabled 21 | file: src=/etc/nginx/sites-available/{{ project_name }} dest=/etc/nginx/sites-enabled/{{ project_name }} state=link 22 | notify: Reload Nginx 23 | 24 | - name: Ensure Nginx service is started, enable service on restart 25 | service: name=nginx state=restarted enabled=yes 26 | when: enabled 27 | 28 | # needs to notify handler to come after the restart handlers 29 | - name: Stop nginx for local dev, disable service 30 | service: name=nginx state=stopped enabled=no 31 | notify: Stop Nginx 32 | when: not enabled 33 | -------------------------------------------------------------------------------- /.provisioning/roles/nginx/templates/site.conf.j2: -------------------------------------------------------------------------------- 1 | upstream appserver { 2 | server localhost:8000 fail_timeout=0; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name {{ host_name }}; 8 | 9 | access_log /var/log/nginx/{{ project_name }}.access.log; 10 | error_log /var/log/nginx/{{ project_name }}.error.log info; 11 | 12 | keepalive_timeout 5; 13 | 14 | # nginx should serve up static files and never send to the WSGI server 15 | location /static { 16 | alias {{ project_path }}/static; 17 | } 18 | 19 | location / { 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header Host $http_host; 22 | proxy_redirect off; 23 | proxy_read_timeout 180s; 24 | 25 | if (!-f $request_filename) { 26 | proxy_pass http://appserver; 27 | break; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /.provisioning/roles/nginx/templates/site_ssl.conf.j2: -------------------------------------------------------------------------------- 1 | upstream appserver { 2 | server localhost:8000 fail_timeout=0; 3 | } 4 | 5 | server { 6 | listen 80; 7 | return 301 https://$host$request_uri; 8 | } 9 | 10 | server { 11 | listen 443 ssl deferred; 12 | server_name {{ host_name }}; 13 | 14 | ssl_certificate {{ home_path }}/{{ project_name }}.crt; 15 | ssl_certificate_key {{ home_path }}/{{ project_name }}.key; 16 | ssl_session_cache shared:SSL:32m; 17 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 18 | ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; 19 | ssl_prefer_server_ciphers on; 20 | 21 | access_log /var/log/nginx/{{ project_name }}.access.log; 22 | error_log /var/log/nginx/{{ project_name }}.error.log info; 23 | 24 | keepalive_timeout 5; 25 | 26 | # nginx should serve up static files and never send to the WSGI server 27 | location /static { 28 | alias {{ project_path }}/static; 29 | } 30 | 31 | location / { 32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 | proxy_set_header Host $http_host; 34 | proxy_redirect off; 35 | proxy_read_timeout 180s; 36 | 37 | if (!-f $request_filename) { 38 | proxy_pass http://appserver; 39 | break; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /.provisioning/roles/nginx/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # server name for nginx 4 | host_name: "localhost" -------------------------------------------------------------------------------- /.provisioning/roles/python/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install common python packages 4 | apt: 5 | state: latest 6 | name: 7 | - python3-dev 8 | - python3-pip 9 | - python3-pycurl 10 | 11 | - name: Delete all existing .pyc files 12 | command: find . -name '*.pyc' -delete 13 | args: 14 | chdir: "{{ project_path }}" 15 | changed_when: false 16 | 17 | - name: Install virtualenv (latest from pip) 18 | pip: executable=pip3 name=virtualenv 19 | 20 | - name: Create the virtualenv 21 | command: virtualenv {{ virtualenv_path }} creates={{ virtualenv_path }}/bin/activate 22 | become: no 23 | 24 | - name: Install packages from requirements.txt inside virtualenv 25 | pip: virtualenv={{ virtualenv_path }} requirements={{ requirements_file }} 26 | become: no 27 | 28 | - name: Activate virtualenv on login 29 | lineinfile: dest=~/.bashrc line='. {{ virtualenv_path }}/bin/activate' 30 | become: no 31 | -------------------------------------------------------------------------------- /.provisioning/roles/python/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | virtualenv_path: "{{ home_path }}/venv" 4 | requirements_file: "{{ project_path }}/requirements.txt" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-Vagrant-Setup 2 | 3 | [![total number of forks](https://img.shields.io/github/forks/clovisphere/simple-flask-vagrant-setup.svg?logo=appveyor&style=for-the-badge)](https://github.com/clovisphere/simple-flask-vagrant-setup/network) [![GitHub stars](https://img.shields.io/github/stars/clovisphere/simple-flask-vagrant-setup.svg?logo=appveyor&style=for-the-badge)](https://github.com/clovisphere/simple-flask-vagrant-setup/stargazers) 4 | 5 | Configure a Flask app on a VM using [Vagrant](https://www.vagrantup.com/), with provisioning handled by [Ansible](https://www.ansible.com/) i.e _setting up [python](https://www.python.org/), [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/), [nginx](https://nginx.org/en/), [gunicorn](http://gunicorn.org/), etc._ 6 | 7 | ## Prerequisite 8 | You need to install: 9 | - [Git](https://git-scm.com/) 10 | - [Vagrant](https://www.vagrantup.com/downloads.html) 11 | 12 | I will be using [VirtualBox](https://www.virtualbox.org/wiki/VirtualBox) for this setup, you can grab a copy for your OS [here](https://www.virtualbox.org/wiki/Downloads). 13 | 14 | ### Quick start 15 | 1. clone this repo and [cd](https://www.wikiwand.com/en/Cd_(command)) into it: 16 | ``` 17 | git clone git@github.com:clovisphere/Flask-Vagrant-Setup.git && cd Flask-Vagrant-Setup 18 | ``` 19 | 2. Boot up your Vagrant environment: 20 | ``` 21 | vagrant up 22 | ``` 23 | 24 | (*) If you get an HTTP 500, try doing a: 25 | 26 | ```vagrant up --provision``` or ```vagrant reload --provision``` 27 | 28 | _This may take less or more than a minute depending on your internet connection (so be patient)._ 29 | 30 | Vagrant runs the virtual machine without a UI. To prove that it is running, you can SSH into the machine: 31 | ``` 32 | vagrant ssh 33 | ``` 34 | You'd be seeing a welcome message like: 35 | ``` 36 | Welcome to Ubuntu 14.04.5 LTS (GNU/Linux 3.13.0-139-generic x86_64) 37 | . 38 | . 39 | . 40 | . 41 | ``` 42 | 43 | Now logout by typing: `logout` 44 | 45 | ### Access app 46 | Point your browser to: [http://10.0.0.5](http://10.0.0.5), you'd see a fancy "Hello" message:-) 47 | 48 | 49 | ##### Credit 50 | * [Aaron Oxborrow](https://github.com/paste), without whom I would have spent many more hours trying to understand *vagrant-ansible* provisioning. 51 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | VAGRANT_IP = "10.0.0.5" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | config.vm.box = "ubuntu/trusty64" 9 | config.vm.network :private_network, ip: VAGRANT_IP 10 | 11 | # -- if you don't fancy private ip, you can use the below -- 12 | # -- note however that by doing so, the 'provision' IP will need to change -- 13 | 14 | # port forwarding to allow access to the app running on the guest OS 15 | # from a dedicated port on the host machine 16 | # config.vm.network "forwarded_port", guest: 80, host: 8080 17 | 18 | # run Ansible from the Vagrant VM 19 | config.vm.provision "ansible_local" do |ansible| 20 | ansible.install = true 21 | ansible.playbook = ".provisioning/deploy.yml" 22 | end 23 | 24 | # add localhost to Ansible inventory 25 | config.vm.provision "shell", inline: "printf 'localhost\n' | sudo tee /etc/ansible/hosts > /dev/null" 26 | 27 | # increase VM RAM size 28 | config.vm.provider "virtualbox" do |vb| 29 | vb.memory = "2048" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /demo-flask-app/public/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clovisphere/simple-flask-vagrant-setup/b46d66e6f203ffbf116e833bbe3a142696f8978b/demo-flask-app/public/__init__.py -------------------------------------------------------------------------------- /demo-flask-app/public/hello.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | 6 | @app.route('/') 7 | def hello(): 8 | return "

Hello There!

" 9 | -------------------------------------------------------------------------------- /demo-flask-app/requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.7 2 | Flask==2.2.5 3 | gunicorn==23.0.0 4 | itsdangerous==0.24 5 | Jinja2>=2.10.1 6 | MarkupSafe==1.0 7 | Werkzeug==3.0.6 8 | -------------------------------------------------------------------------------- /demo-flask-app/wsgi.py: -------------------------------------------------------------------------------- 1 | """This is the app entry point""" 2 | 3 | from public.hello import app 4 | 5 | if __name__ == '__main__': 6 | app.run() 7 | --------------------------------------------------------------------------------