Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.git
.github
.gitignore
*.pyc
__pycache__
*.pyo
*.pyd
.Python
*.egg-info
dist/
build/
.tox
.pytest_cache
.coverage
htmlcov/
.env
.env.*
!.env.example
debian/
*.md
TODO
AUTHORS
COPYING
MANIFEST.in
tox.ini
patchman-client.spec
run/
38 changes: 38 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Required - generate with: python -c "import secrets; print(secrets.token_urlsafe(50))"
SECRET_KEY=change-me-to-a-random-secret-key

# Required
DB_PASSWORD=change-me-to-a-secure-password

# Database (defaults shown)
DB_NAME=patchman
DB_USER=patchman

# Web server
WEB_PORT=8000
ALLOWED_HOSTS=*
DEBUG=False
TIME_ZONE=UTC

# Gunicorn
GUNICORN_WORKERS=4
GUNICORN_THREADS=2
GUNICORN_TIMEOUT=120

# Celery
CELERY_LOG_LEVEL=info
CELERY_POOL_TYPE=threads
CELERY_CONCURRENCY=4

# Patchman
REQUIRE_API_KEY=True
CACHE_MIDDLEWARE_SECONDS=0
MAX_MIRRORS=2
MAX_MIRROR_FAILURES=14
DAYS_WITHOUT_REPORT=14

# Errata - comma-separated, remove unwanted OS types
ERRATA_OS_UPDATES=yum,rocky,alma,arch,ubuntu,debian
ALMA_RELEASES=8,9,10
DEBIAN_CODENAMES=bookworm,trixie
UBUNTU_CODENAMES=jammy,noble
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ pyvenv.cfg
.vscode
.venv
*.xml
.env
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
DockerfileFROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
libmagic1 \
libpq-dev \
gcc \
git \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn whitenoise psycopg2-binary

COPY . .

# Overwrite the local_settings.py with the Docker-specific one.
# settings.py resolves conf_path to ./etc/patchman when running from source.
COPY docker/local_settings.py /app/etc/patchman/local_settings.py

RUN mkdir -p /var/lib/patchman/static

COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 8000

ENTRYPOINT ["/entrypoint.sh"]
105 changes: 105 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
services:

db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${DB_NAME:-patchman}
POSTGRES_USER: ${DB_USER:-patchman}
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-patchman} -d ${DB_NAME:-patchman}"]
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5

web:
build: .
command: web
restart: unless-stopped
ports:
- "${WEB_PORT:-8000}:8000"
volumes:
- static_files:/var/lib/patchman/static
environment:
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY is required}
DB_NAME: ${DB_NAME:-patchman}
DB_USER: ${DB_USER:-patchman}
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
DB_HOST: db
DB_PORT: "5432"
REDIS_HOST: redis
REDIS_PORT: "6379"
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-*}
DEBUG: ${DEBUG:-False}
TIME_ZONE: ${TIME_ZONE:-UTC}
GUNICORN_WORKERS: ${GUNICORN_WORKERS:-4}
GUNICORN_THREADS: ${GUNICORN_THREADS:-2}
GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-120}
REQUIRE_API_KEY: ${REQUIRE_API_KEY:-True}
CACHE_MIDDLEWARE_SECONDS: ${CACHE_MIDDLEWARE_SECONDS:-0}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy

celery-worker:
build: .
command: celery-worker
restart: unless-stopped
environment:
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY is required}
DB_NAME: ${DB_NAME:-patchman}
DB_USER: ${DB_USER:-patchman}
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
DB_HOST: db
DB_PORT: "5432"
REDIS_HOST: redis
REDIS_PORT: "6379"
CELERY_LOG_LEVEL: ${CELERY_LOG_LEVEL:-info}
CELERY_POOL_TYPE: ${CELERY_POOL_TYPE:-threads}
CELERY_CONCURRENCY: ${CELERY_CONCURRENCY:-4}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy

celery-beat:
build: .
command: celery-beat
restart: unless-stopped
environment:
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY is required}
DB_NAME: ${DB_NAME:-patchman}
DB_USER: ${DB_USER:-patchman}
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
DB_HOST: db
DB_PORT: "5432"
REDIS_HOST: redis
REDIS_PORT: "6379"
CELERY_LOG_LEVEL: ${CELERY_LOG_LEVEL:-info}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy

volumes:
postgres_data:
redis_data:
static_files:
80 changes: 80 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash
set -e

# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL..."
until python -c "
import psycopg2, os, sys
try:
psycopg2.connect(
host=os.environ['DB_HOST'],
port=os.environ.get('DB_PORT', '5432'),
dbname=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
)
sys.exit(0)
except Exception:
sys.exit(1)
" 2>/dev/null; do
echo "PostgreSQL unavailable - retrying in 2s..."
sleep 2
done
echo "PostgreSQL is ready."

# Wait for Redis to be ready
echo "Waiting for Redis..."
until python -c "
import redis, os, sys
try:
r = redis.Redis(host=os.environ.get('REDIS_HOST', 'redis'), port=int(os.environ.get('REDIS_PORT', 6379)))
r.ping()
sys.exit(0)
except Exception:
sys.exit(1)
" 2>/dev/null; do
echo "Redis unavailable - retrying in 2s..."
sleep 2
done
echo "Redis is ready."

CMD="${1:-web}"

if [ "$CMD" = "web" ]; then
echo "Running migrations..."
python manage.py migrate --noinput

echo "Collecting static files..."
python manage.py collectstatic --noinput

echo "Starting Gunicorn..."
exec gunicorn patchman.wsgi \
--bind 0.0.0.0:8000 \
--workers "${GUNICORN_WORKERS:-4}" \
--threads "${GUNICORN_THREADS:-2}" \
--timeout "${GUNICORN_TIMEOUT:-120}" \
--access-logfile - \
--error-logfile -

elif [ "$CMD" = "celery-worker" ]; then
echo "Starting Celery worker..."
exec celery \
--app patchman \
worker \
--loglevel "${CELERY_LOG_LEVEL:-info}" \
--pool "${CELERY_POOL_TYPE:-threads}" \
--concurrency "${CELERY_CONCURRENCY:-4}" \
--task-events \
--hostname "patchman-worker@%h"

elif [ "$CMD" = "celery-beat" ]; then
echo "Starting Celery beat..."
exec celery \
--app patchman \
beat \
--loglevel "${CELERY_LOG_LEVEL:-info}" \
--scheduler django_celery_beat.schedulers:DatabaseScheduler

else
exec "$@"
fi
Loading