├── .dockerignore ├── .gitignore ├── README.md ├── app ├── Dockerfile.flask ├── requirements.txt ├── run.sh ├── server.py └── wsgi.py ├── build.sh ├── docker-compose.yaml ├── git-hooks └── post-receive ├── nginx ├── Dockerfile.nginx └── nginx.conf ├── pi-server.code-workspace └── pyvenv.cfg /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | bin/ 4 | include/ 5 | lib/ 6 | 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | bin/ 4 | include/ 5 | lib/ 6 | 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Pi Server Logo](https://static.codingforentrepreneurs.com/media/projects/pi-server/images/share/The_Pi_Server_-_Share.jpg)](https://www.codingforentrepreneurs.com/projects/pi-server) 2 | 3 | # Pi Server 4 | 5 | Turn your raspberry pi into a networked server! Learn to deploy a minimal Python application into your Pi. We'll cover using nginx, supervisor, git (including on your server), git hooks, docker & docker compose, and more! 6 | 7 | ### Resources 8 | - Pi Server [tutorial series](https://www.codingforentrepreneurs.com/projects/pi-server) 9 | - Docker & Docker Compose [tutorial series](https://www.codingforentrepreneurs.com/projects/pi-server) 10 | - Pi Awesome - [Guides, Resources, & More](https://www.piawesome.com) 11 | - Related [repo](https://github.com/codingforentrepreneurs/Pi-Awesome) 12 | -------------------------------------------------------------------------------- /app/Dockerfile.flask: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | RUN python3 -m venv . 7 | RUN /app/bin/pip install pip --upgrade 8 | RUN /app/bin/pip install -r requirements.txt 9 | 10 | RUN chmod +x run.sh 11 | 12 | CMD ["./run.sh"] -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | gunicorn 3 | requests -------------------------------------------------------------------------------- /app/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | RUN_PORT=${PORT:-8000} 3 | exec /app/bin/gunicorn --bind 0.0.0.0:$RUN_PORT wsgi:app -------------------------------------------------------------------------------- /app/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | import requests 4 | app = Flask(__name__) 5 | 6 | @app.route("/") 7 | def hello_world(): 8 | env_name = os.environ.get("NAME") 9 | return f"Hello world again from {env_name}" 10 | 11 | @app.route("/about") 12 | def about_us(): 13 | return "This this about us" 14 | 15 | 16 | @app.route("/another") 17 | def another_view(): 18 | return "Yet another route" 19 | 20 | 21 | @app.route("/lookup") 22 | def lookup_view(): 23 | r = requests.get("http://raspberrypi") 24 | return r"STATUS {r.status_code}" 25 | 26 | if __name__=="__main__": 27 | app.run(host="0.0.0.0", port=8000) -------------------------------------------------------------------------------- /app/wsgi.py: -------------------------------------------------------------------------------- 1 | from server import app 2 | 3 | 4 | if __name__=="__main__": 5 | app.run() -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOCKER_COMPOSE_CHANGED=$(git -C /var/repos/flaskapp.git/ diff --name-only HEAD~1 HEAD | grep "docker-compose.yaml") 4 | 5 | 6 | NGINX_GIT_CHANGED=$(git -C /var/repos/flaskapp.git/ diff --name-only HEAD~1 HEAD | grep "nginx/") 7 | NGINX_RUNNING=$(docker ps | grep nginx) 8 | 9 | APP_CODE_CHANGED=$(git -C /var/repos/flaskapp.git/ diff --name-only HEAD~1 HEAD | grep "app/") 10 | 11 | if [[ $DOCKER_COMPOSE_CHANGED ]]; then 12 | echo "Docker compose has changed, rebuilding..." 13 | docker-compose down 14 | docker-compose up -d --build 15 | fi 16 | 17 | if [[ $NGINX_GIT_CHANGED ]]; then 18 | echo "Nginx has changed, rebuilding..." 19 | docker-compose down 20 | docker-compose up -d --build 21 | fi 22 | 23 | if [[ $NGINX_RUNNING == "" ]]; then 24 | echo "Nginx is not running. Bringing Up" 25 | docker-compose up -d --build 26 | fi 27 | 28 | FLASKSERVICE_RUNNING=$(docker ps | grep flaskservice) 29 | 30 | if [[ $FLASKSERVICE_RUNNING == "" ]]; then 31 | echo "Flask app service is not running. Bringing Up" 32 | docker-compose up -d --build flaskservice dosservice tresservice 33 | docker-compose exec -d nginx nginx -s reload 34 | fi 35 | 36 | BACKUP_SERVER_RUNNING=$(docker ps | grep backupservice) 37 | if [[ $BACKUP_SERVER_RUNNING == "" ]]; then 38 | echo "Backup service is not running. Bringing Up" 39 | docker-compose up -d --build backupservice 40 | docker-compose exec -d nginx nginx -s reload 41 | fi 42 | 43 | if [[ $APP_CODE_CHANGED ]]; then 44 | echo "Flask service code changed, rebuilding service" 45 | docker-compose build flaskservice dosservice tresservice backupservice 46 | docker-compose stop flaskservice dosservice tresservice 47 | docker-compose rm -f flaskservice dosservice tresservice 48 | docker-compose up -d --no-deps flaskservice dosservice tresservice 49 | docker-compose exec -d nginx nginx -s reload 50 | if [[ $(docker ps | grep flaskservice) ]]; then 51 | echo "Flask service rebuilt and up, rebuilding backup" 52 | docker-compose stop backupservice 53 | docker-compose rm -f backupservice 54 | docker-compose up -d --build backupservice 55 | docker-compose exec -d nginx nginx -s reload 56 | fi 57 | fi 58 | 59 | sleep 5 60 | docker-compose exec -d nginx nginx -s reload -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | flaskservice: 4 | restart: 'always' 5 | build: 6 | context: ./app 7 | dockerfile: Dockerfile.flask 8 | environment: 9 | - PORT=8001 10 | - NAME='primary service' 11 | expose: 12 | - 8001 13 | dosservice: 14 | restart: 'always' 15 | build: 16 | context: ./app 17 | dockerfile: Dockerfile.flask 18 | environment: 19 | - PORT=8003 20 | - NAME='dos service' 21 | expose: 22 | - 8003 23 | tresservice: 24 | restart: 'always' 25 | build: 26 | context: ./app 27 | dockerfile: Dockerfile.flask 28 | environment: 29 | - PORT=8004 30 | - NAME='tres service' 31 | expose: 32 | - 8004 33 | backupservice: 34 | restart: 'always' 35 | build: 36 | context: ./app 37 | dockerfile: Dockerfile.flask 38 | environment: 39 | - PORT=8002 40 | - NAME='backup service' 41 | expose: 42 | - 8002 43 | nginx: 44 | restart: 'always' 45 | build: 46 | context: ./nginx 47 | dockerfile: Dockerfile.nginx 48 | ports: 49 | - 80:80 50 | depends_on: 51 | - flaskservice 52 | - dosservice 53 | - backupservice -------------------------------------------------------------------------------- /git-hooks/post-receive: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git --work-tree=/var/www/flaskapp/ --git-dir=/var/repos/flaskapp.git/ checkout main -f 4 | 5 | cd /var/www/flaskapp/ 6 | bash build.sh -------------------------------------------------------------------------------- /nginx/Dockerfile.nginx: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream flaskproxy { 2 | server flaskservice:8001; 3 | server dosservice:8003; 4 | server tresservice:8004; 5 | server backupservice:8002 backup; 6 | } 7 | 8 | server { 9 | listen 80 default_server; 10 | listen [::]:80 default_server; 11 | 12 | root /var/www/html; 13 | 14 | # Add index.php to the list if you are using PHP 15 | index index.html index.htm index.nginx-debian.html; 16 | 17 | server_name _; 18 | 19 | location / { 20 | proxy_pass http://flaskproxy; 21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | proxy_set_header Host $host; 23 | proxy_redirect off; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /pi-server.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /Library/Frameworks/Python.framework/Versions/3.9/bin 2 | include-system-site-packages = false 3 | version = 3.9.2 4 | --------------------------------------------------------------------------------