mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-10-01 10:18:41 +00:00
ci: update compose.yml for prod deployment
- Add Nginx as a reverse proxy - Add cert-selfsign.sh for generating self-signed certificates - Add certbot-issue.sh for Let's Encrypt certificate issuance and renewal - Add cron-install.sh and cron-uninstall.sh for system cron management - Add .env.example as an environment variable template
This commit is contained in:
@@ -23,12 +23,10 @@ nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.log
|
||||
.git
|
||||
.gitignore
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.hypothesis
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@@ -43,8 +41,12 @@ venv.bak/
|
||||
*~
|
||||
docs/
|
||||
tests/
|
||||
*.md
|
||||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
pipeline/built_assets/
|
||||
nginx/
|
||||
*.pem
|
||||
*.key
|
||||
.env.*
|
||||
scripts/
|
||||
|
||||
26
.env.example
Normal file
26
.env.example
Normal file
@@ -0,0 +1,26 @@
|
||||
SECRET_KEY=
|
||||
DEBUG=false
|
||||
STAGING=false
|
||||
DJANGO_ALLOWED_HOSTS=
|
||||
DOMAIN=
|
||||
SESSION_COOKIE_SECURE_ENABLED=true
|
||||
CSRF_COOKIE_SECURE_ENABLED=true
|
||||
|
||||
DATABASE_ENGINE=postgresql
|
||||
DATABASE_NAME=rigs
|
||||
DATABASE_USERNAME=rigs
|
||||
DATABASE_PASSWORD=
|
||||
DATABASE_HOST=db
|
||||
DATABASE_PORT=5432
|
||||
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_USE_SSL=
|
||||
EMAIL_FROM=
|
||||
|
||||
HCAPTCHA_SITEKEY=
|
||||
HCAPTCHA_SECRET=
|
||||
SENTRY_DSN=
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -104,4 +104,13 @@ screenshots/
|
||||
|
||||
# Virutal Environments
|
||||
.venv/
|
||||
/.env
|
||||
|
||||
# Environment variable files (keep .env.example)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
nginx/certs/
|
||||
nginx/certbot/
|
||||
*.pem
|
||||
*.key
|
||||
@@ -13,8 +13,7 @@ from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
# FIXME This needs a different implementation when moved off heroku
|
||||
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
|
||||
help = 'Sends email reminders as required. Triggered daily through sys cron in production.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
events = models.Event.objects.current_events().select_related('riskassessment')
|
||||
|
||||
82
compose.yml
82
compose.yml
@@ -1,41 +1,67 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:17
|
||||
image: postgres:18.6
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
POSTGRES_USER: ${DATABASE_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
pyrigs:
|
||||
build: .
|
||||
container_name: pyrigs
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
|
||||
DEBUG: ${DEBUG}
|
||||
DJANGO_LOGLEVEL: ${DJANGO_LOGLEVEL}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS}
|
||||
DATABASE_ENGINE: ${DATABASE_ENGINE}
|
||||
DOMAIN: ${DOMAIN:-example.com}
|
||||
DEBUG: ${DEBUG:-false}
|
||||
STAGING: ${STAGING:-false}
|
||||
SESSION_COOKIE_SECURE_ENABLED: ${SESSION_COOKIE_SECURE_ENABLED:-true}
|
||||
CSRF_COOKIE_SECURE_ENABLED: ${CSRF_COOKIE_SECURE_ENABLED:-true}
|
||||
DATABASE_ENGINE: ${DATABASE_ENGINE:-postgresql}
|
||||
DATABASE_NAME: ${DATABASE_NAME}
|
||||
DATABASE_USERNAME: ${DATABASE_USERNAME}
|
||||
|
||||
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
|
||||
DATABASE_HOST: ${DATABASE_HOST}
|
||||
DATABASE_PORT: ${DATABASE_PORT}
|
||||
env_file:
|
||||
- .env
|
||||
DATABASE_HOST: ${DATABASE_HOST:-db}
|
||||
DATABASE_PORT: ${DATABASE_PORT:-5432}
|
||||
EMAIL_HOST: ${EMAIL_HOST}
|
||||
EMAIL_PORT: ${EMAIL_PORT:-25}
|
||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
||||
EMAIL_USE_TLS: ${EMAIL_USE_TLS:-false}
|
||||
EMAIL_USE_SSL: ${EMAIL_USE_SSL:-false}
|
||||
EMAIL_FROM: ${EMAIL_FROM}
|
||||
HCAPTCHA_SITEKEY: ${HCAPTCHA_SITEKEY:-10000000-ffff-ffff-ffff-000000000001}
|
||||
HCAPTCHA_SECRET: ${HCAPTCHA_SECRET:-0x0000000000000000000000000000000000000000}
|
||||
SENTRY_DSN: ${SENTRY_DSN:-}
|
||||
expose:
|
||||
- "8000"
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
python manage.py migrate --noinput &&
|
||||
gunicorn \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--workers 2 \
|
||||
PyRIGS.wsgi
|
||||
|
||||
develop:
|
||||
# Create a `watch` configuration to update the app
|
||||
#
|
||||
watch:
|
||||
# Sync the working directory with the `/app` directory in the container
|
||||
- action: sync
|
||||
@@ -48,5 +74,21 @@ services:
|
||||
# Rebuild the image on changes to the `pyproject.toml`
|
||||
- action: rebuild
|
||||
path: ./pyproject.toml
|
||||
|
||||
nginx:
|
||||
image: nginx:stable-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./nginx/certs:/etc/nginx/certs:ro
|
||||
- ./nginx/certbot:/var/www/certbot:ro
|
||||
|
||||
depends_on:
|
||||
- pyrigs
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
38
nginx/default.conf.example
Normal file
38
nginx/default.conf.example
Normal file
@@ -0,0 +1,38 @@
|
||||
upstream pyrigs {
|
||||
server pyrigs:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Let's Encrypt HTTP-01
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
client_max_body_size 20m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://pyrigs;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
28
scripts/cert-selfsign.sh
Normal file
28
scripts/cert-selfsign.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage: ./scripts/cert-selfsign.sh [domain] (no root required)
|
||||
set -euo pipefail
|
||||
|
||||
DOMAIN="${1:-localhost}"
|
||||
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
CERTS_DIR="$REPO_DIR/nginx/certs"
|
||||
|
||||
mkdir -p "$CERTS_DIR"
|
||||
|
||||
if [ -f "$CERTS_DIR/fullchain.pem" ]; then
|
||||
read -rp "$CERTS_DIR/fullchain.pem already exists, overwrite? [y/N] " ans
|
||||
case "$ans" in
|
||||
y|Y) ;;
|
||||
*) echo "Aborted"; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 30 \
|
||||
-keyout "$CERTS_DIR/privkey.pem" \
|
||||
-out "$CERTS_DIR/fullchain.pem" \
|
||||
-subj "/CN=$DOMAIN" \
|
||||
-addext "subjectAltName=DNS:$DOMAIN" \
|
||||
2>/dev/null
|
||||
chmod 600 "$CERTS_DIR/privkey.pem"
|
||||
|
||||
echo "Done: self-signed certificate (valid for 30 days) generated in $CERTS_DIR"
|
||||
echo "Next: start the stack with 'docker compose up -d', then run scripts/certbot-issue.sh to switch to the real certificate"
|
||||
47
scripts/certbot-issue.sh
Normal file
47
scripts/certbot-issue.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Issue the initial certificate (run as root on the deployment server)
|
||||
# Usage: sudo ./scripts/certbot-issue.sh <domain> <email>
|
||||
set -euo pipefail
|
||||
|
||||
DOMAIN="${1:?Usage: $0 <domain> <email>}"
|
||||
EMAIL="${2:?Usage: $0 <domain> <email>}"
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WEBROOT="$REPO_DIR/nginx/certbot"
|
||||
CERTS_DIR="$REPO_DIR/nginx/certs"
|
||||
HOOK_PATH="/etc/letsencrypt/renewal-hooks/deploy/pyrigs-deploy.sh"
|
||||
|
||||
if ! command -v certbot >/dev/null 2>&1; then
|
||||
echo "certbot not found. Install it first: apt install certbot (or snap install certbot --classic)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$WEBROOT" "$CERTS_DIR" /etc/letsencrypt/renewal-hooks/deploy
|
||||
|
||||
# 1. Initial issuance (certbot skips if the cert already exists and is not due for renewal)
|
||||
certbot certonly \
|
||||
--webroot -w "$WEBROOT" \
|
||||
-d "$DOMAIN" \
|
||||
-m "$EMAIL" \
|
||||
--agree-tos --no-eff-email --non-interactive
|
||||
|
||||
# 2. Install deploy-hook (placed in the official dir, triggered automatically on successful renewal)
|
||||
cat > "$HOOK_PATH" <<EOF
|
||||
#!/bin/sh
|
||||
# Generated by scripts/certbot-issue.sh: copy renewed certs and reload nginx
|
||||
set -e
|
||||
cp -fL "\$RENEWED_LINEAGE/fullchain.pem" "$CERTS_DIR/fullchain.pem"
|
||||
cp -fL "\$RENEWED_LINEAGE/privkey.pem" "$CERTS_DIR/privkey.pem"
|
||||
chmod 600 "$CERTS_DIR/privkey.pem"
|
||||
docker compose -f "$REPO_DIR/compose.yml" exec -T nginx nginx -s reload || true
|
||||
echo "pyrigs deploy-hook: \$(date) renewed [\$RENEWED_DOMAINS] and reloaded nginx"
|
||||
EOF
|
||||
chmod +x "$HOOK_PATH"
|
||||
|
||||
# 3. Deploy the freshly issued certificate right away
|
||||
RENEWED_LINEAGE="/etc/letsencrypt/live/$DOMAIN" \
|
||||
RENEWED_DOMAINS="$DOMAIN" \
|
||||
"$HOOK_PATH"
|
||||
|
||||
echo "Done: certificate for $DOMAIN issued and deployed to $CERTS_DIR; deploy-hook installed."
|
||||
echo "Tip: for scheduled renewal, run sudo ./scripts/cron-install.sh"
|
||||
34
scripts/cron-install.sh
Normal file
34
scripts/cron-install.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install system-level scheduled tasks (run as root on the deployment server)
|
||||
# Usage: sudo ./scripts/cron-install.sh
|
||||
#
|
||||
# Writes two /etc/cron.d files:
|
||||
# 1. pyrigs-certbot - certificate renewal check, daily at 03:00 / 15:00
|
||||
# 2. pyrigs-django - cleanup and reminder jobs inside the pyrigs container
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
CERTBOT_CRON="/etc/cron.d/pyrigs-certbot"
|
||||
DJANGO_CRON="/etc/cron.d/pyrigs-django"
|
||||
|
||||
# 1. Certificate renewal: checked twice a day (officially recommended frequency);
|
||||
# on successful renewal the deploy-hook in the official hook dir takes over
|
||||
cat > "$CERTBOT_CRON" <<EOF
|
||||
# Generated by scripts/cron-install.sh; remove with scripts/cron-uninstall.sh
|
||||
0 3,15 * * * root certbot renew --quiet
|
||||
EOF
|
||||
|
||||
# 2. Django scheduled tasks (replacing the former Heroku Scheduler jobs, run via exec inside the container)
|
||||
cat > "$DJANGO_CRON" <<EOF
|
||||
SHELL=/bin/sh
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
# Generated by scripts/cron-install.sh; remove with scripts/cron-uninstall.sh
|
||||
0 0 * * * root docker compose -f $REPO_DIR/compose.yml exec -T pyrigs sh -c "python manage.py cleanupregistration && python manage.py usercleanup" >> /var/log/pyrigs-cleanup.log 2>&1
|
||||
0 8 * * * root docker compose -f $REPO_DIR/compose.yml exec -T pyrigs python manage.py send_reminders >> /var/log/pyrigs-reminders.log 2>&1
|
||||
EOF
|
||||
|
||||
chmod 644 "$CERTBOT_CRON" "$DJANGO_CRON"
|
||||
|
||||
echo "Installed:"
|
||||
echo " $CERTBOT_CRON"
|
||||
echo " $DJANGO_CRON"
|
||||
19
scripts/cron-uninstall.sh
Normal file
19
scripts/cron-uninstall.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uninstall the scheduled tasks installed by cron-install.sh (run as root on the deployment server)
|
||||
# Usage: sudo ./scripts/cron-uninstall.sh
|
||||
set -euo pipefail
|
||||
|
||||
CERTBOT_CRON="/etc/cron.d/pyrigs-certbot"
|
||||
DJANGO_CRON="/etc/cron.d/pyrigs-django"
|
||||
HOOK_PATH="/etc/letsencrypt/renewal-hooks/deploy/pyrigs-deploy.sh"
|
||||
|
||||
rm -f "$CERTBOT_CRON" "$DJANGO_CRON"
|
||||
echo "Removed scheduled tasks: $CERTBOT_CRON $DJANGO_CRON"
|
||||
|
||||
if [ -f "$HOOK_PATH" ]; then
|
||||
read -rp "Also remove the deploy-hook ($HOOK_PATH)? [y/N] " ans
|
||||
case "$ans" in
|
||||
y|Y) rm -f "$HOOK_PATH"; echo "Deploy-hook removed" ;;
|
||||
*) echo "Deploy-hook kept" ;;
|
||||
esac
|
||||
fi
|
||||
Reference in New Issue
Block a user