feat: refactor Dockerfile and supervisord configuration to remove cron and add periodic sync script

This commit is contained in:
Sean Morley
2026-01-09 11:59:25 -05:00
parent 50b5a95c49
commit 12ff50ba1c
5 changed files with 117 additions and 24 deletions

View File

@@ -41,7 +41,7 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies (including GDAL and cron)
# Install runtime dependencies (including GDAL)
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
gdal-bin \
@@ -49,7 +49,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
nginx \
memcached \
supervisor \
cron \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy Python packages from builder
@@ -61,12 +60,8 @@ COPY ./server /code/
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY ./entrypoint.sh /code/entrypoint.sh
COPY ./crontab /etc/cron.d/adventurelog-cron
RUN chmod +x /code/entrypoint.sh \
&& mkdir -p /code/static /code/media \
&& chmod 0644 /etc/cron.d/adventurelog-cron \
&& crontab /etc/cron.d/adventurelog-cron \
&& touch /var/log/cron.log
&& mkdir -p /code/static /code/media
# Collect static files
RUN python3 manage.py collectstatic --noinput --verbosity 2
@@ -74,5 +69,8 @@ RUN python3 manage.py collectstatic --noinput --verbosity 2
# Expose ports
EXPOSE 80 8000
# Start Supervisor
# Start with an entrypoint that runs init tasks then starts supervisord
ENTRYPOINT ["/code/entrypoint.sh"]
# Start supervisord to manage processes
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -1,8 +0,0 @@
# AdventureLog cron jobs
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Run sync_visited_regions daily at midnight
0 0 * * * cd /code && /usr/local/bin/python3 manage.py sync_visited_regions >> /var/log/cron.log 2>&1
# Empty line required at end of cron file

View File

@@ -84,8 +84,4 @@ fi
cat /code/adventurelog.txt
# Start Gunicorn in foreground
exec gunicorn main.wsgi:application \
--bind [::]:8000 \
--workers 2 \
--timeout 120
exec "$@"

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Periodic sync runner for AdventureLog.
Runs sync_visited_regions management command every 60 seconds.
Managed by supervisord to ensure it inherits container environment variables.
"""
import os
import sys
import time
import logging
import signal
import threading
from datetime import datetime, timedelta
from pathlib import Path
# Setup Django
sys.path.insert(0, str(Path(__file__).parent))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
import django
django.setup()
from django.core.management import call_command
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger(__name__)
INTERVAL_SECONDS = 60
# Event used to signal shutdown from signal handlers
_stop_event = threading.Event()
def _seconds_until_next_midnight() -> float:
"""Return number of seconds until the next local midnight."""
now = datetime.now()
next_midnight = (now + timedelta(days=1)).replace(
hour=0, minute=0, second=0, microsecond=0
)
return (next_midnight - now).total_seconds()
def _handle_termination(signum, frame):
"""Signal handler for SIGTERM and SIGINT: request graceful shutdown."""
logger.info(f"Received signal {signum}; shutting down gracefully...")
_stop_event.set()
def run_sync():
"""Run the sync_visited_regions command."""
try:
logger.info("Running sync_visited_regions...")
call_command('sync_visited_regions')
logger.info("Sync completed successfully")
except Exception as e:
logger.error(f"Sync failed: {e}", exc_info=True)
def main():
"""Main loop - run sync every INTERVAL_SECONDS."""
logger.info(f"Starting periodic sync (interval: {INTERVAL_SECONDS}s)")
# Install signal handlers so supervisord (or other process managers)
# can request a clean shutdown using SIGTERM/SIGINT.
signal.signal(signal.SIGTERM, _handle_termination)
signal.signal(signal.SIGINT, _handle_termination)
try:
while not _stop_event.is_set():
# Wait until the next local midnight (or until shutdown)
wait_seconds = _seconds_until_next_midnight()
hours = wait_seconds / 3600.0
logger.info(
f"Next sync scheduled in {wait_seconds:.0f}s (~{hours:.2f}h) at local midnight"
)
# Sleep until midnight or until stop event is set
if _stop_event.wait(wait_seconds):
break
# It's midnight (or we woke up), run the sync once
run_sync()
# After running at midnight, loop continues to compute next midnight
except Exception:
logger.exception("Unexpected error in periodic sync loop")
finally:
logger.info("Periodic sync worker exiting")
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
# Fallback in case the signal is delivered as KeyboardInterrupt
logger.info("KeyboardInterrupt received — exiting")
_stop_event.set()
except SystemExit:
logger.info("SystemExit received — exiting")
finally:
logger.info("run_periodic_sync terminated")

View File

@@ -8,7 +8,8 @@ stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
[program:gunicorn]
command=/code/entrypoint.sh
command=/usr/local/bin/gunicorn main.wsgi:application --bind [::]:8000 --workers 2 --timeout 120
directory=/code
autorestart=true
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
@@ -23,8 +24,9 @@ stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
[program:cron]
command=cron -f
[program:sync_visited_regions]
command=/usr/local/bin/python3 /code/run_periodic_sync.py
directory=/code
autorestart=true
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr