Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deployment script and configuration #34

Merged
merged 46 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
d0fb23f
Add deployment script
Joe-Heffer-Shef Jan 8, 2025
aa78a03
Fix syntax error
Joe-Heffer-Shef Jan 8, 2025
0d7fd29
Add shell and systemd linting
Joe-Heffer-Shef Jan 8, 2025
4476fd6
Add gunicorn systemd service
Joe-Heffer-Shef Jan 8, 2025
d75a0f4
Don't use pyproject.toml
Joe-Heffer-Shef Jan 8, 2025
a84d3f9
Install Gunicorn socket
Joe-Heffer-Shef Jan 8, 2025
df44486
-
Joe-Heffer-Shef Jan 8, 2025
249c5d8
Fix SORT module name
Joe-Heffer-Shef Jan 8, 2025
3ac712f
Configure NGINX proxy pass
Joe-Heffer-Shef Jan 8, 2025
e2ca956
-
Joe-Heffer-Shef Jan 8, 2025
d363d54
Fix nginx linter path
Joe-Heffer-Shef Jan 8, 2025
83cb30f
Fix indent
Joe-Heffer-Shef Jan 8, 2025
4398da4
Reload services on deploy
Joe-Heffer-Shef Jan 8, 2025
6251386
Revert Gunicorn socket path
Joe-Heffer-Shef Jan 8, 2025
b2dc7cb
Add ALLOWED_HOSTS env var
Joe-Heffer-Shef Jan 8, 2025
2e45292
Add deployment auto check workflow
Joe-Heffer-Shef Jan 8, 2025
7dbc1f7
Install Django package
Joe-Heffer-Shef Jan 8, 2025
3cac085
Install dotenv
Joe-Heffer-Shef Jan 8, 2025
9f87479
Serve static files
Joe-Heffer-Shef Jan 8, 2025
bcbfeeb
Remove spoof check
Joe-Heffer-Shef Jan 8, 2025
3479a76
Collect static files
Joe-Heffer-Shef Jan 8, 2025
bc9ecc8
Collect static files on service start
Joe-Heffer-Shef Jan 8, 2025
b4b7099
Install static files in deployment script
Joe-Heffer-Shef Jan 8, 2025
a557a3e
Start working on PostgreSQL installation
Joe-Heffer-Shef Jan 8, 2025
813669b
Configure database using env vars
Joe-Heffer-Shef Jan 9, 2025
68b147f
Forward HTTP host to proxy
Joe-Heffer-Shef Jan 9, 2025
e74e8d9
Update deployment docs
Joe-Heffer-Shef Jan 9, 2025
4a4b8b2
Create UTF-8 en_GB database localisation
Joe-Heffer-Shef Jan 9, 2025
19af2be
Ensure gunicorn is started
Joe-Heffer-Shef Jan 9, 2025
7924f60
-
Joe-Heffer-Shef Jan 9, 2025
e5e9948
String-to-Boolean: accept any input data type
Joe-Heffer-Shef Jan 10, 2025
c803dae
Run linting on PRs and main branch only
Joe-Heffer-Shef Jan 10, 2025
a43a48f
Merge remote-tracking branch 'origin/feat/deploy' into feat/deploy
Joe-Heffer-Shef Jan 10, 2025
e70549d
Lock down the personality(2) system call
Joe-Heffer-Shef Jan 10, 2025
2976280
Add deployment check tool
Joe-Heffer-Shef Jan 13, 2025
59467c4
Add cookie security settings
Joe-Heffer-Shef Jan 13, 2025
fbe965a
Make to-Boolean function more robust
Joe-Heffer-Shef Jan 13, 2025
13b96cf
Make to-Boolean function more robust
Joe-Heffer-Shef Jan 13, 2025
0e7a2a2
SECRET_KEY has min length 50 chars
Joe-Heffer-Shef Jan 13, 2025
5b19e89
Add SSL cert config
Joe-Heffer-Shef Jan 13, 2025
e29cbf4
Suggest Python version 3.12
Joe-Heffer-Shef Jan 13, 2025
4d3adc2
Disable HTTP2
Joe-Heffer-Shef Jan 13, 2025
b497927
Add note on self-signed certificates
Joe-Heffer-Shef Jan 13, 2025
bc19e29
Format file
Joe-Heffer-Shef Jan 13, 2025
d825074
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/deploy
Joe-Heffer-Shef Jan 13, 2025
fabcd5a
Merge branch 'dev' into feat/deploy
f-allian Jan 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/lint-nginx.yaml
Joe-Heffer-Shef marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# GitHub Actions workflow for validating NGINX configuration files
# https://github.com/jhinch/nginx-linter
name: Lint NGINX config files
on: [ push ]
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install nginx-linter
run: npm install -g nginx-linter
- name: Run nginx linter
run: nginx-linter --include config/nginx/* --no-follow-includes
14 changes: 14 additions & 0 deletions .github/workflows/lint-shell-scripts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Lint shell scripts
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions
# https://github.com/marketplace/actions/shell-linter
name: Lint shell scripts
on: push
jobs:
lint_shell:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: azohra/shell-linter@v0.6.0
with:
severity: 'warning'
exclude-paths: 'LICENSE'
14 changes: 14 additions & 0 deletions .github/workflows/lint-systemd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# GitHub Actions workflow for linting the systemd unit files
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions
name: Lint systemd units
on: [ push ]
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install systemdlint
run: pip install systemdlint==1.*
- name: Lint systemd units
run: systemdlint ./config/systemd/*
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ python manage.py createsuperuser

6. Create a `.env` file in the project root directory and add the following environment variables:

```
```bash
DJANGO_SECRET_KEY=your_secret_key
DJANGO_DEBUG=True
```

---
Expand All @@ -81,3 +82,7 @@ The app will be available at http://127.0.0.1:8000.
```bash
python manage.py loaddata data\questionnaires.json data\questionnaires.json
```

# Deployment

Please read [`docs/deployment.md`](docs/deployment.md).
45 changes: 23 additions & 22 deletions SORT/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,30 @@
from dotenv import load_dotenv
import os

load_dotenv() # Load environment variables from .env file

def string_to_boolean(s: str) -> bool:
"""
Check if the string value is 1, yes, or true.
"""
return s.casefold()[0] in {"1", "y", "t"}
Joe-Heffer-Shef marked this conversation as resolved.
Show resolved Hide resolved


# Load environment variables from .env file
load_dotenv(os.getenv('DJANGO_ENV_PATH'))

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []
DEBUG = string_to_boolean(os.getenv("DJANGO_DEBUG", "False"))

ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '').split()

# Application definition

Expand Down Expand Up @@ -82,21 +89,19 @@
},
]

WSGI_APPLICATION = "SORT.wsgi.application"

WSGI_APPLICATION = os.getenv("DJANGO_WSGI_APPLICATION", "SORT.wsgi.application")

# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
# Set the database settings using environment variables, or default to a local SQLite database file.
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / 'db.sqlite3',
# "NAME": os.getenv("DB_NAME"),
# "USER": os.getenv("DB_USER"),
# "PASSWORD": os.getenv("DB_PASSWORD"),
# "HOST": os.getenv("DB_HOST"),
# "PORT": os.getenv("DB_PORT"),
"ENGINE": os.getenv("DJANGO_DATABASE_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.getenv("DJANGO_DATABASE_NAME", BASE_DIR / 'db.sqlite3'),
"USER": os.getenv("DJANGO_DATABASE_USER"),
"PASSWORD": os.getenv("DJANGO_DATABASE_PASSWORD"),
"HOST": os.getenv("DJANGO_DATABASE_HOST"),
"PORT": os.getenv("DJANGO_DATABASE_PORT"),
}
}

Expand All @@ -118,7 +123,6 @@
},
]


# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

Expand All @@ -130,16 +134,13 @@

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"

STATICFILES_DIRS = [BASE_DIR / 'static']



# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

Expand All @@ -148,14 +149,13 @@
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"


# FA: End session when the browser is closed
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

# FA: 30 minutes before automatic log out
SESSION_COOKIE_AGE = 1800

PASSWORD_RESET_TIMEOUT = 1800 # FA: default to expire after 30 minutes
PASSWORD_RESET_TIMEOUT = 1800 # FA: default to expire after 30 minutes

# FA: for local testing emails:

Expand All @@ -174,5 +174,6 @@

# FA: for production:

#EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"

STATIC_ROOT = os.getenv('DJANGO_STATIC_ROOT')
2 changes: 2 additions & 0 deletions SORT/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "SORT.settings")

# Create WSGI application object
application = get_wsgi_application()
"https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/#the-application-object"
3 changes: 3 additions & 0 deletions config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Server configuration

This directory contains configuration files for the production web server.
43 changes: 43 additions & 0 deletions config/nginx/gunicorn.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# https://docs.gunicorn.org/en/stable/deploy.html#nginx-configuration
upstream app_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response
# for UNIX domain socket setups
server unix:/run/gunicorn.sock fail_timeout=0;
}

server {
# https://nginx.org/en/docs/http/ngx_http_core_module.html#listen
listen 80 deferred;
client_max_body_size 4G;
server_name sort-web-app.shef.ac.uk www.sort-web-app.shef.ac.uk;
keepalive_timeout 5;

# /server-status endpoint
# This is used by IT Services to monitor servers using collectd
# https://nginx.org/en/docs/http/ngx_http_stub_status_module.html
# https://www.collectd.org/documentation/manpages/collectd.conf.html
# It's based on Apache mod_status https://httpd.apache.org/docs/2.4/mod/mod_status.html
location = /server-status {
stub_status;
}

# Serve static files without invoking Python WSGI
location /static/ {
# https://nginx.org/en/docs/http/ngx_http_core_module.html#root
root /var/www/sort;
}

# Proxy forward to the WSGI Python app
location / {
# Set HTTP headers for the proxied service
# https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://app_server;
}
}
31 changes: 31 additions & 0 deletions config/systemd/gunicorn.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This is a systemd unit that defines the Gunicorn service.
# https://docs.gunicorn.org/en/stable/deploy.html#systemd
# https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html

[Unit]
Description=gunicorn service
Requires=gunicorn.socket
After=network.target

[Service]
# gunicorn can let systemd know when it is ready
Type=notify
NotifyAccess=main
# the specific user that our service will run as
User=gunicorn
Group=gunicorn
# this user can be transiently created by systemd
DynamicUser=true
RuntimeDirectory=gunicorn
WorkingDirectory=/opt/sort
ExecStart=/opt/sort/venv/bin/gunicorn SORT.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
# if your app does not need administrative capabilities, let systemd know
ProtectSystem=strict
Delegate=yes

[Install]
WantedBy=multi-user.target
17 changes: 17 additions & 0 deletions config/systemd/gunicorn.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# https://docs.gunicorn.org/en/stable/deploy.html#systemd
# https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html
[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock
# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation.
# Only the nginx daemon will need access to the socket:
SocketUser=www-data
SocketGroup=www-data
# Once the user/group is correct, restrict the permissions:
SocketMode=0660

[Install]
WantedBy=sockets.target
64 changes: 64 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -e

# SORT deployment script for Ubuntu 22.04 LTS
# See: How to deploy Django
# https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/#the-application-object

# Usage:
# Clone the repository
# git clone git@github.com:RSE-Sheffield/SORT.git
# cd SORT
# sudo bash -x deploy.sh

# Options
sort_dir="/opt/sort"
venv_dir="$sort_dir/venv"
pip="$venv_dir/bin/pip"
python_version="python3.12"
python="$venv_dir/bin/python"

# Install British UTF-8 locale so we can use this with PostgreSQL.
# This is important to avoid the limitations of the LATIN1 character set.
sudo locale-gen en_GB
sudo locale-gen en_GB.UTF-8
sudo update-locale

# Create Python virtual environment
apt update -qq
apt install --upgrade --yes -qq "$python_version" "$python_version-venv"
python3 -m venv "$venv_dir"

# Install the SORT Django app package
$pip install --quiet -r requirements.txt
cp --recursive * "$sort_dir/"

# Install static files into DJANGO_STATIC_ROOT
(cd "$sort_dir" && exec $python manage.py collectstatic --no-input)

# Install Gunicorn service
cp --verbose config/systemd/gunicorn.service /etc/systemd/system/gunicorn.service
cp --verbose config/systemd/gunicorn.socket /etc/systemd/system/gunicorn.socket
systemctl daemon-reload
systemctl enable gunicorn.service
systemctl enable gunicorn.socket
systemctl start gunicorn.service
systemctl reload gunicorn.service

# Install web reverse proxy server
# Install nginx
# https://nginx.org/en/docs/install.html
apt install --yes -qq nginx

# Configure web server
rm -f /etc/nginx/sites-enabled/default
cp config/nginx/*.conf /etc/nginx/sites-available
# Enable the site by creating a symbolic link
ln --symbolic --force /etc/nginx/sites-available/gunicorn.conf /etc/nginx/sites-enabled/gunicorn.conf
systemctl reload nginx.service

# Install PostgreSQL database
# https://ubuntu.com/server/docs/install-and-configure-postgresql
apt install --yes -qq postgresql
# Restart PostgreSQL to enable any new locales
systemctl restart postgresql
Loading