feat: add dev and prod deployment scaffolding
This commit is contained in:
46
.env.dev.example
Normal file
46
.env.dev.example
Normal file
@@ -0,0 +1,46 @@
|
||||
DJANGO_SECRET_KEY=change-me
|
||||
DJANGO_DEBUG=1
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=
|
||||
DJANGO_SECURE_COOKIES=0
|
||||
DJANGO_SECURE_SSL_REDIRECT=0
|
||||
SESSION_IDLE_TIMEOUT_SECONDS=1800
|
||||
SENSITIVE_ACTION_REAUTH_SECONDS=1200
|
||||
|
||||
POSTGRES_DB=workdock
|
||||
POSTGRES_USER=workdock
|
||||
POSTGRES_PASSWORD=workdock
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
CELERY_TASK_ALWAYS_EAGER=0
|
||||
RATE_LIMIT_ENABLED=1
|
||||
RATE_LIMIT_LOGIN_LIMIT=8
|
||||
RATE_LIMIT_LOGIN_WINDOW=300
|
||||
RATE_LIMIT_PASSWORD_RESET_LIMIT=5
|
||||
RATE_LIMIT_PASSWORD_RESET_WINDOW=600
|
||||
RATE_LIMIT_ADMIN_ACTION_LIMIT=20
|
||||
RATE_LIMIT_ADMIN_ACTION_WINDOW=300
|
||||
|
||||
EMAIL_HOST=mailhog
|
||||
EMAIL_PORT=1025
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
EMAIL_USE_TLS=0
|
||||
EMAIL_USE_SSL=0
|
||||
DEFAULT_FROM_EMAIL=onboarding@example.local
|
||||
TEST_NOTIFICATION_EMAIL=hr@example.local
|
||||
IT_ONBOARDING_NOTIFICATION_EMAIL=it@workdock.de
|
||||
GENERAL_INFO_NOTIFICATION_EMAIL=info@workdock.de
|
||||
BUSINESS_CARD_NOTIFICATION_EMAIL=cards@workdock.de
|
||||
HR_WORKS_NOTIFICATION_EMAIL=hr@workdock.de
|
||||
KEY_NOTIFICATION_EMAIL=keys@workdock.de
|
||||
|
||||
NEXTCLOUD_BASE_URL=https://nextcloud.example.com/remote.php/dav/files/onboarding
|
||||
NEXTCLOUD_USERNAME=onboarding@example.com
|
||||
NEXTCLOUD_PASSWORD=change-me
|
||||
NEXTCLOUD_DIRECTORY=Group-on-off-boarding
|
||||
NEXTCLOUD_ENABLED=0
|
||||
|
||||
PDF_OUTPUT_DIR=/app/media/pdfs
|
||||
48
.env.prod.example
Normal file
48
.env.prod.example
Normal file
@@ -0,0 +1,48 @@
|
||||
DJANGO_SECRET_KEY=change-me-long-random-value
|
||||
DJANGO_DEBUG=0
|
||||
DJANGO_ALLOWED_HOSTS=workdock.example.com
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://workdock.example.com
|
||||
DJANGO_SECURE_COOKIES=1
|
||||
DJANGO_SECURE_SSL_REDIRECT=1
|
||||
SESSION_IDLE_TIMEOUT_SECONDS=1800
|
||||
SENSITIVE_ACTION_REAUTH_SECONDS=1200
|
||||
|
||||
POSTGRES_DB=workdock
|
||||
POSTGRES_USER=workdock
|
||||
POSTGRES_PASSWORD=change-me-db-password
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
CELERY_TASK_ALWAYS_EAGER=0
|
||||
RATE_LIMIT_ENABLED=1
|
||||
RATE_LIMIT_LOGIN_LIMIT=8
|
||||
RATE_LIMIT_LOGIN_WINDOW=300
|
||||
RATE_LIMIT_PASSWORD_RESET_LIMIT=5
|
||||
RATE_LIMIT_PASSWORD_RESET_WINDOW=600
|
||||
RATE_LIMIT_ADMIN_ACTION_LIMIT=20
|
||||
RATE_LIMIT_ADMIN_ACTION_WINDOW=300
|
||||
|
||||
EMAIL_HOST=smtp.example.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=mailer@example.com
|
||||
EMAIL_HOST_PASSWORD=change-me
|
||||
EMAIL_USE_TLS=1
|
||||
EMAIL_USE_SSL=0
|
||||
DEFAULT_FROM_EMAIL=onboarding@example.com
|
||||
TEST_NOTIFICATION_EMAIL=hr@example.com
|
||||
IT_ONBOARDING_NOTIFICATION_EMAIL=it@example.com
|
||||
GENERAL_INFO_NOTIFICATION_EMAIL=info@example.com
|
||||
BUSINESS_CARD_NOTIFICATION_EMAIL=cards@example.com
|
||||
HR_WORKS_NOTIFICATION_EMAIL=hr@example.com
|
||||
KEY_NOTIFICATION_EMAIL=keys@example.com
|
||||
|
||||
NEXTCLOUD_BASE_URL=https://nextcloud.example.com/remote.php/dav/files/onboarding
|
||||
NEXTCLOUD_USERNAME=onboarding@example.com
|
||||
NEXTCLOUD_PASSWORD=change-me
|
||||
NEXTCLOUD_DIRECTORY=Group-on-off-boarding
|
||||
NEXTCLOUD_ENABLED=1
|
||||
|
||||
PDF_OUTPUT_DIR=/app/media/pdfs
|
||||
APP_PORT=8088
|
||||
SITE_ADDRESS=workdock.example.com
|
||||
48
.env.test.example
Normal file
48
.env.test.example
Normal file
@@ -0,0 +1,48 @@
|
||||
DJANGO_SECRET_KEY=change-me-long-random-value
|
||||
DJANGO_DEBUG=0
|
||||
DJANGO_ALLOWED_HOSTS=192.168.2.55,localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://192.168.2.55:8088
|
||||
DJANGO_SECURE_COOKIES=0
|
||||
DJANGO_SECURE_SSL_REDIRECT=0
|
||||
SESSION_IDLE_TIMEOUT_SECONDS=1800
|
||||
SENSITIVE_ACTION_REAUTH_SECONDS=1200
|
||||
|
||||
POSTGRES_DB=workdock
|
||||
POSTGRES_USER=workdock
|
||||
POSTGRES_PASSWORD=change-me-db-password
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
CELERY_TASK_ALWAYS_EAGER=0
|
||||
RATE_LIMIT_ENABLED=1
|
||||
RATE_LIMIT_LOGIN_LIMIT=8
|
||||
RATE_LIMIT_LOGIN_WINDOW=300
|
||||
RATE_LIMIT_PASSWORD_RESET_LIMIT=5
|
||||
RATE_LIMIT_PASSWORD_RESET_WINDOW=600
|
||||
RATE_LIMIT_ADMIN_ACTION_LIMIT=20
|
||||
RATE_LIMIT_ADMIN_ACTION_WINDOW=300
|
||||
|
||||
EMAIL_HOST=mailhog
|
||||
EMAIL_PORT=1025
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
EMAIL_USE_TLS=0
|
||||
EMAIL_USE_SSL=0
|
||||
DEFAULT_FROM_EMAIL=onboarding@test.local
|
||||
TEST_NOTIFICATION_EMAIL=hr@test.local
|
||||
IT_ONBOARDING_NOTIFICATION_EMAIL=it@test.local
|
||||
GENERAL_INFO_NOTIFICATION_EMAIL=info@test.local
|
||||
BUSINESS_CARD_NOTIFICATION_EMAIL=cards@test.local
|
||||
HR_WORKS_NOTIFICATION_EMAIL=hr@test.local
|
||||
KEY_NOTIFICATION_EMAIL=keys@test.local
|
||||
|
||||
NEXTCLOUD_BASE_URL=
|
||||
NEXTCLOUD_USERNAME=
|
||||
NEXTCLOUD_PASSWORD=
|
||||
NEXTCLOUD_DIRECTORY=
|
||||
NEXTCLOUD_ENABLED=0
|
||||
|
||||
PDF_OUTPUT_DIR=/app/media/pdfs
|
||||
APP_PORT=8088
|
||||
SITE_ADDRESS=:80
|
||||
33
.github/workflows/deploy-prod.yml
vendored
Normal file
33
.github/workflows/deploy-prod.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Deploy Production
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-prod
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Deploy over SSH
|
||||
uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.PROD_DEPLOY_HOST }}
|
||||
username: ${{ secrets.PROD_DEPLOY_USER }}
|
||||
key: ${{ secrets.PROD_DEPLOY_SSH_KEY }}
|
||||
port: ${{ secrets.PROD_DEPLOY_PORT || 22 }}
|
||||
script: |
|
||||
set -e
|
||||
REPO_URL="git@github.com:${{ github.repository }}.git"
|
||||
DEPLOY_DIR="${{ secrets.PROD_DEPLOY_PATH }}"
|
||||
if [ ! -d "$DEPLOY_DIR/.git" ]; then
|
||||
git clone "$REPO_URL" "$DEPLOY_DIR"
|
||||
fi
|
||||
cd "$DEPLOY_DIR"
|
||||
git fetch --all --prune
|
||||
git checkout main || git checkout -b main origin/main
|
||||
git reset --hard origin/main
|
||||
RUN_DJANGO_CHECK=1 ./scripts/deploy_stack.sh .env.prod docker-compose.prod.yml
|
||||
36
.github/workflows/deploy-test.yml
vendored
Normal file
36
.github/workflows/deploy-test.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Deploy Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
concurrency:
|
||||
group: deploy-test-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
steps:
|
||||
- name: Deploy over SSH
|
||||
uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.TEST_DEPLOY_HOST }}
|
||||
username: ${{ secrets.TEST_DEPLOY_USER }}
|
||||
key: ${{ secrets.TEST_DEPLOY_SSH_KEY }}
|
||||
port: ${{ secrets.TEST_DEPLOY_PORT || 22 }}
|
||||
script: |
|
||||
set -e
|
||||
REPO_URL="git@github.com:${{ github.repository }}.git"
|
||||
DEPLOY_DIR="${{ secrets.TEST_DEPLOY_PATH }}"
|
||||
if [ ! -d "$DEPLOY_DIR/.git" ]; then
|
||||
git clone "$REPO_URL" "$DEPLOY_DIR"
|
||||
fi
|
||||
cd "$DEPLOY_DIR"
|
||||
git fetch --all --prune
|
||||
git checkout develop || git checkout -b develop origin/develop
|
||||
git reset --hard ${{ github.sha }}
|
||||
RUN_DJANGO_CHECK=0 DEPLOY_HEALTH_URL="http://127.0.0.1:8088/healthz/" ./scripts/deploy_stack.sh .env.test docker-compose.prod.yml
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,9 @@ __pycache__/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.dev.example
|
||||
!.env.test.example
|
||||
!.env.prod.example
|
||||
.venv/
|
||||
venv/
|
||||
db.sqlite3
|
||||
|
||||
68
DEPLOYMENT.md
Normal file
68
DEPLOYMENT.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Deployment
|
||||
|
||||
## Branch strategy
|
||||
- `develop`: test/staging deployments
|
||||
- `main`: production deployments
|
||||
- keep one private GitHub repository
|
||||
|
||||
## Local development
|
||||
- current `docker-compose.yml` remains the development stack
|
||||
- use `.env` or `.env.dev.example` as the template
|
||||
|
||||
## Test deployment server
|
||||
- copy `.env.test.example` to `.env.test` on the server
|
||||
- recommended path: `/opt/workdock/.env.test`
|
||||
- current test target: `192.168.2.55`
|
||||
|
||||
## Production deployment server
|
||||
- copy `.env.prod.example` to `.env.prod` on the server
|
||||
- use HTTPS and secure cookies in production
|
||||
|
||||
## Server bootstrap
|
||||
Install:
|
||||
- `git`
|
||||
- `docker`
|
||||
- `docker compose plugin`
|
||||
- `curl`
|
||||
|
||||
Recommended app directory:
|
||||
- `/opt/workdock`
|
||||
|
||||
## First clone on server
|
||||
```bash
|
||||
git clone git@github.com:OWNER/REPO.git /opt/workdock
|
||||
cd /opt/workdock
|
||||
cp .env.test.example .env.test
|
||||
```
|
||||
|
||||
## Test deploy manually
|
||||
```bash
|
||||
cd /opt/workdock
|
||||
./scripts/deploy_stack.sh .env.test docker-compose.prod.yml
|
||||
```
|
||||
|
||||
## Production deploy manually
|
||||
```bash
|
||||
cd /opt/workdock
|
||||
./scripts/deploy_stack.sh .env.prod docker-compose.prod.yml
|
||||
```
|
||||
|
||||
## GitHub Actions secrets
|
||||
### Development environment
|
||||
- `TEST_DEPLOY_HOST`
|
||||
- `TEST_DEPLOY_USER`
|
||||
- `TEST_DEPLOY_SSH_KEY`
|
||||
- `TEST_DEPLOY_PORT`
|
||||
- `TEST_DEPLOY_PATH`
|
||||
|
||||
### Production environment
|
||||
- `PROD_DEPLOY_HOST`
|
||||
- `PROD_DEPLOY_USER`
|
||||
- `PROD_DEPLOY_SSH_KEY`
|
||||
- `PROD_DEPLOY_PORT`
|
||||
- `PROD_DEPLOY_PATH`
|
||||
|
||||
## Important note for the test server
|
||||
`.env.test.example` is intentionally configured for an HTTP LAN test deployment.
|
||||
That means `RUN_DJANGO_CHECK=0` is used in the test deploy workflow, because the application security checks require secure cookies when `DEBUG=0`.
|
||||
For real production, use `.env.prod` behind HTTPS and keep `RUN_DJANGO_CHECK=1`.
|
||||
@@ -15,5 +15,5 @@ COPY requirements.txt /tmp/requirements.txt
|
||||
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
||||
|
||||
COPY . /app
|
||||
RUN chmod +x /app/entrypoint-web.sh /app/entrypoint-worker.sh
|
||||
RUN chmod +x /app/entrypoint-web.sh /app/entrypoint-worker.sh /app/entrypoint-web-prod.sh /app/entrypoint-worker-prod.sh
|
||||
RUN chown -R app:app /app
|
||||
|
||||
9
backend/entrypoint-web-prod.sh
Executable file
9
backend/entrypoint-web-prod.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
exec gunicorn config.wsgi:application \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--workers 3 \
|
||||
--timeout 120 \
|
||||
--access-logfile - \
|
||||
--error-logfile -
|
||||
4
backend/entrypoint-worker-prod.sh
Executable file
4
backend/entrypoint-worker-prod.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
exec celery -A config worker -l info
|
||||
15
deploy/Caddyfile
Normal file
15
deploy/Caddyfile
Normal file
@@ -0,0 +1,15 @@
|
||||
{$SITE_ADDRESS} {
|
||||
encode gzip zstd
|
||||
|
||||
handle_path /static/* {
|
||||
root * /srv/static
|
||||
file_server
|
||||
}
|
||||
|
||||
handle_path /media/* {
|
||||
root * /srv/media
|
||||
file_server
|
||||
}
|
||||
|
||||
reverse_proxy web:8000
|
||||
}
|
||||
72
docker-compose.prod.yml
Normal file
72
docker-compose.prod.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2.10-alpine
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${APP_ENV_FILE:-.env.prod}
|
||||
ports:
|
||||
- "${APP_PORT:-8088}:80"
|
||||
volumes:
|
||||
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- media_data:/srv/media:ro
|
||||
- static_data:/srv/static:ro
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./backend
|
||||
command: sh -c "./entrypoint-web-prod.sh"
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${APP_ENV_FILE:-.env.prod}
|
||||
volumes:
|
||||
- media_data:/app/media
|
||||
- static_data:/app/staticfiles
|
||||
- backup_data:/app/backups
|
||||
user: "app"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz/', timeout=5)\""]
|
||||
interval: 20s
|
||||
timeout: 8s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ./backend
|
||||
command: sh -c "./entrypoint-worker-prod.sh"
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${APP_ENV_FILE:-.env.prod}
|
||||
volumes:
|
||||
- media_data:/app/media
|
||||
- static_data:/app/staticfiles
|
||||
- backup_data:/app/backups
|
||||
user: "app"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-workdock}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-workdock}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-workdock}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
media_data:
|
||||
static_data:
|
||||
backup_data:
|
||||
37
scripts/deploy_stack.sh
Executable file
37
scripts/deploy_stack.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="${1:-.env.prod}"
|
||||
COMPOSE_FILE="${2:-docker-compose.prod.yml}"
|
||||
HEALTH_URL="${DEPLOY_HEALTH_URL:-http://127.0.0.1:${APP_PORT:-8088}/healthz/}"
|
||||
RUN_DJANGO_CHECK="${RUN_DJANGO_CHECK:-1}"
|
||||
export APP_ENV_FILE="$ENV_FILE"
|
||||
COMPOSE=(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE")
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "Missing env file: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${COMPOSE[@]}" build web worker caddy
|
||||
"${COMPOSE[@]}" up -d db redis
|
||||
"${COMPOSE[@]}" run --rm web python manage.py migrate --noinput
|
||||
"${COMPOSE[@]}" run --rm web python manage.py bootstrap_initial_users
|
||||
"${COMPOSE[@]}" run --rm web python manage.py collectstatic --noinput
|
||||
|
||||
if [[ "$RUN_DJANGO_CHECK" == "1" ]]; then
|
||||
"${COMPOSE[@]}" run --rm web python manage.py check
|
||||
fi
|
||||
|
||||
"${COMPOSE[@]}" up -d web worker caddy
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
if curl --fail --silent --show-error --max-time 5 "$HEALTH_URL" >/dev/null; then
|
||||
echo "Deployment healthy: $HEALTH_URL"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Health check did not become ready in time: $HEALTH_URL" >&2
|
||||
exit 1
|
||||
Reference in New Issue
Block a user