From 0a52c4da7b86d6fdcc0ee820356446889bbaf4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 12 May 2026 17:47:23 +0200 Subject: [PATCH 1/5] Make container more robust --- .dockerignore | 1 - Caddyfile | 23 ++++++++++++----------- Caddyfile.dev | 15 +++++++++++++++ Dockerfile | 23 ++++++++++++++++------- Makefile | 7 ++++--- docker-compose.yml | 32 +++++++++++--------------------- entrypoint.sh | 36 ++++++++++++++++++------------------ supervisor.conf | 40 ++++++++++++++++++++++++++++++++++++++++ timetracker/settings.py | 6 +++--- 9 files changed, 119 insertions(+), 64 deletions(-) create mode 100644 Caddyfile.dev create mode 100644 supervisor.conf diff --git a/.dockerignore b/.dockerignore index 82c32d2..66786d0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,6 @@ static .drone.yml .editorconfig .gitignore -Caddyfile CHANGELOG.md db.sqlite3 docker-compose* diff --git a/Caddyfile b/Caddyfile index fbcfe47..3ba5d36 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,14 +1,15 @@ -{ - auto_https off - admin off +{ + auto_https off } :8000 { - handle_path /static/* { - root * /usr/share/caddy - file_server - } - handle { - reverse_proxy backend:8001 - } -} \ No newline at end of file + handle_path /static/* { + root * /home/timetracker/app/static + file_server + } + handle /robots.txt { + root * /home/timetracker/app/games/static + file_server + } + reverse_proxy localhost:8001 +} diff --git a/Caddyfile.dev b/Caddyfile.dev new file mode 100644 index 0000000..1e2b212 --- /dev/null +++ b/Caddyfile.dev @@ -0,0 +1,15 @@ +{ + auto_https off +} + +:8000 { + handle_path /static/* { + root * static + file_server browse + } + handle /robots.txt { + root * games/static + file_server browse + } + reverse_proxy :8001 +} diff --git a/Dockerfile b/Dockerfile index 41d5756..cf36cc1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,20 +22,29 @@ ENV PROD=1 \ PYTHONUNBUFFERED=1 \ PATH="/home/timetracker/app/.venv/bin:$PATH" -RUN useradd -m --uid 1000 timetracker \ - && mkdir -p /var/www/django/static \ - && chown timetracker:timetracker /var/www/django/static - +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libcap2-bin \ + supervisor \ + && rm -rf /var/lib/apt/lists/* \ + && useradd -m --uid 1000 timetracker \ + && mkdir -p /var/log/supervisor /etc/supervisor/conf.d /home/timetracker/data \ + && chown timetracker:timetracker /var/log/supervisor /home/timetracker/data + +RUN curl -sL "https://caddyserver.com/api/download?os=linux&arch=amd64" \ + -o /usr/local/bin/caddy && chmod +x /usr/local/bin/caddy + WORKDIR /home/timetracker/app COPY --from=builder --chown=timetracker:timetracker /home/timetracker/app /home/timetracker/app +COPY --chown=timetracker:timetracker Caddyfile /etc/caddy/Caddyfile +COPY --chown=timetracker:timetracker supervisor.conf /etc/supervisor/conf.d/supervisor.conf COPY --chown=timetracker:timetracker entrypoint.sh / RUN chmod +x /entrypoint.sh -USER timetracker - ENV VERSION_NUMBER=1.6.1 EXPOSE 8000 -CMD [ "/entrypoint.sh" ] +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Makefile b/Makefile index af9ccd6..123d209 100644 --- a/Makefile +++ b/Makefile @@ -41,9 +41,10 @@ caddy: dev-prod: migrate collectstatic @npx concurrently \ - --names "Django,Django-Q" \ - "PROD=1 uv run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker" - "uv run manage.py qcluster" + --names "Caddy,Django,Django-Q" \ + "caddy run --config Caddyfile.dev" \ + "PROD=1 uv run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker" \ + "PROD=1 uv run manage.py qcluster" dumpgames: uv run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml diff --git a/docker-compose.yml b/docker-compose.yml index 41470e1..9f8d1da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,20 @@ --- services: - backend: - image: registry.kucharczyk.xyz/timetracker + timetracker: + image: ${REGISTRY_URL:-registry.kucharczyk.xyz}/timetracker:1.7.0 build: context: . dockerfile: Dockerfile + container_name: timetracker environment: - - TZ=Europe/Prague - - CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz" - user: "1000" + - TZ=${TZ:-Europe/Prague} + - CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz + - PUID=${PUID:-1000} + - PGID=${PGID:-100} + ports: + - "${TIMETRACKER_EXTERNAL_PORT:-8000}:8000" volumes: - - "static-files:/var/www/django/static" - - "$PWD/db.sqlite3:/home/timetracker/app/db.sqlite3" + - "${DOCKER_STORAGE_PATH:-/tmp}/timetracker/data:/home/timetracker/data" + - "${DOCKER_STORAGE_PATH:-/tmp}/timetracker/backups:/home/timetracker/app/games/fixtures/backups" restart: unless-stopped - frontend: - image: caddy - volumes: - - "static-files:/usr/share/caddy:ro" - - "$PWD/Caddyfile:/etc/caddy/Caddyfile" - ports: - - "8000:8000" - depends_on: - - backend - -volumes: - static-files: - - \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 12a7e20..3447a2b 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,23 +1,23 @@ #!/bin/bash -# Apply database migrations set -euo pipefail -echo "Apply database migrations" -python manage.py migrate -echo "Collect static files" +PUID=${PUID:-1000} +PGID=${PGID:-100} + +USERHOME=$(grep timetracker /etc/passwd | cut -d ":" -f6) +usermod -d "/root" timetracker +groupmod -o -g "$PGID" timetracker +usermod -o -u "$PUID" timetracker +usermod -d "${USERHOME}" timetracker + +mkdir -p /home/timetracker/data /var/log/supervisor +chmod 755 /home/timetracker/app +chmod 755 /home/timetracker/app/.venv + +chown "$PUID:$PGID" /home/timetracker/data +chown "$PUID:$PGID" /var/log/supervisor + +python manage.py migrate python manage.py collectstatic --clear --no-input -_term() { - echo "Caught SIGTERM signal!" - kill -SIGTERM "$gunicorn_pid" - kill -SIGTERM "$django_q_pid" -} -trap _term SIGTERM - -echo "Starting Django-Q cluster" -python manage.py qcluster & django_q_pid=$! - -echo "Starting app" -python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$! - -wait "$gunicorn_pid" "$django_q_pid" +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf diff --git a/supervisor.conf b/supervisor.conf new file mode 100644 index 0000000..4af9a02 --- /dev/null +++ b/supervisor.conf @@ -0,0 +1,40 @@ +[supervisord] +nodaemon=true +user=root +logfile=/dev/stdout +logfile_maxbytes=0 + +[program:caddy] +command=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile +directory=/home/timetracker/app +autostart=true +autorestart=true +stderr_logfile=/dev/stderr +stdout_logfile=/dev/stdout +stderr_logfile_maxbytes=0 +stdout_logfile_maxbytes=0 +user=timetracker + +[program:gunicorn] +command=python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - +directory=/home/timetracker/app +autostart=true +autorestart=true +stderr_logfile=/dev/stderr +stdout_logfile=/dev/stdout +stderr_logfile_maxbytes=0 +stdout_logfile_maxbytes=0 +process_name=%(program_name)s +user=timetracker + +[program:qcluster] +command=python manage.py qcluster +directory=/home/timetracker/app +autostart=true +autorestart=true +stderr_logfile=/dev/stderr +stdout_logfile=/dev/stdout +stderr_logfile_maxbytes=0 +stdout_logfile_maxbytes=0 +process_name=%(program_name)s +user=timetracker diff --git a/timetracker/settings.py b/timetracker/settings.py index e5defd3..b3719b6 100644 --- a/timetracker/settings.py +++ b/timetracker/settings.py @@ -4,7 +4,7 @@ Django settings for timetracker project. Generated by 'django-admin startproject' using Django 4.1.4. For more information on this file, see -https://docs.djangoproject.com/en/4.1/topics/settings/ +https://docs.djangoproject.com/en/4.1/topics/deployment/checklist/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ @@ -110,7 +110,7 @@ WSGI_APPLICATION = "timetracker.wsgi.application" DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "NAME": Path(os.environ.get("DATA_DIR", "/home/timetracker/data")) / "db.sqlite3", "OPTIONS": { "timeout": 20, "init_command": "PRAGMA synchronous=FULL; PRAGMA journal_mode=WAL;", @@ -154,7 +154,7 @@ USE_TZ = True # https://docs.djangoproject.com/en/4.1/howto/static-files/ STATIC_URL = "static/" -STATIC_ROOT = BASE_DIR / "static" if DEBUG else "/var/www/django/static" +STATIC_ROOT = BASE_DIR / "static" # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field -- 2.52.0 From bf6d20ca58c3b4eb9258aa7b1758777fe21cfd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 12 May 2026 18:04:50 +0200 Subject: [PATCH 2/5] Pin Caddy version --- Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index cf36cc1..6f66af5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && mkdir -p /var/log/supervisor /etc/supervisor/conf.d /home/timetracker/data \ && chown timetracker:timetracker /var/log/supervisor /home/timetracker/data -RUN curl -sL "https://caddyserver.com/api/download?os=linux&arch=amd64" \ - -o /usr/local/bin/caddy && chmod +x /usr/local/bin/caddy +ARG CADDY_VERSION=2.9.1 +RUN curl -sL "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz" \ + -o /tmp/caddy.tar.gz && \ + tar -xzf /tmp/caddy.tar.gz -C /tmp && \ + mv /tmp/caddy /usr/local/bin/caddy && \ + rm /tmp/caddy.tar.gz && \ + chmod +x /usr/local/bin/caddy WORKDIR /home/timetracker/app -- 2.52.0 From 2b43e9a848072fccff3e86f31a0a710361ad06a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 12 May 2026 18:07:54 +0200 Subject: [PATCH 3/5] Add .env.example documenting environment variables --- .env.example | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..07dd15e --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Docker registry URL (used in docker-compose.yml) +REGISTRY_URL=registry.kucharczyk.xyz + +# Container timezone +TZ=Europe/Prague + +# User/group IDs for container (used in entrypoint.sh) +PUID=1000 +PGID=100 + +# External port mapping +TIMETRACKER_EXTERNAL_PORT=8000 + +# Data storage path +DOCKER_STORAGE_PATH=/tmp + +# Django production mode (set to "1" for production) +PROD=1 + +# Database directory (overrides default in settings.py) +DATA_DIR=/home/timetracker/data + +# CSRF trusted origins +CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz -- 2.52.0 From 00f84fee9b7ef5e0d88fcc666a743cf9bd5b27e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 12 May 2026 18:23:54 +0200 Subject: [PATCH 4/5] Make default database location more robust --- .env.example | 7 ++----- .gitignore | 1 + docker-compose.yml | 3 ++- entrypoint.sh | 4 ++-- timetracker/settings.py | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 07dd15e..ddac900 100644 --- a/.env.example +++ b/.env.example @@ -11,14 +11,11 @@ PGID=100 # External port mapping TIMETRACKER_EXTERNAL_PORT=8000 -# Data storage path -DOCKER_STORAGE_PATH=/tmp - # Django production mode (set to "1" for production) PROD=1 -# Database directory (overrides default in settings.py) -DATA_DIR=/home/timetracker/data +# Database directory (defaults to project root) +DATA_DIR=/home/timetracker/app/data # CSRF trusted origins CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz diff --git a/.gitignore b/.gitignore index d74ec92..9caa4e0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ node_modules package-lock.json db.sqlite3 +data/ /static/ dist/ .DS_Store diff --git a/docker-compose.yml b/docker-compose.yml index 9f8d1da..3ca2704 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,10 +11,11 @@ services: - CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz - PUID=${PUID:-1000} - PGID=${PGID:-100} + - DATA_DIR=${DATA_DIR:-/home/timetracker/app/data} ports: - "${TIMETRACKER_EXTERNAL_PORT:-8000}:8000" volumes: - - "${DOCKER_STORAGE_PATH:-/tmp}/timetracker/data:/home/timetracker/data" + - "./data:/home/timetracker/app/data" - "${DOCKER_STORAGE_PATH:-/tmp}/timetracker/backups:/home/timetracker/app/games/fixtures/backups" restart: unless-stopped diff --git a/entrypoint.sh b/entrypoint.sh index 3447a2b..2c3946d 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -10,11 +10,11 @@ groupmod -o -g "$PGID" timetracker usermod -o -u "$PUID" timetracker usermod -d "${USERHOME}" timetracker -mkdir -p /home/timetracker/data /var/log/supervisor +mkdir -p /home/timetracker/app/data /var/log/supervisor chmod 755 /home/timetracker/app chmod 755 /home/timetracker/app/.venv -chown "$PUID:$PGID" /home/timetracker/data +chown "$PUID:$PGID" /home/timetracker/app/data chown "$PUID:$PGID" /var/log/supervisor python manage.py migrate diff --git a/timetracker/settings.py b/timetracker/settings.py index b3719b6..07130f9 100644 --- a/timetracker/settings.py +++ b/timetracker/settings.py @@ -110,7 +110,7 @@ WSGI_APPLICATION = "timetracker.wsgi.application" DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": Path(os.environ.get("DATA_DIR", "/home/timetracker/data")) / "db.sqlite3", + "NAME": Path(os.environ.get("DATA_DIR", str(BASE_DIR))) / "db.sqlite3", "OPTIONS": { "timeout": 20, "init_command": "PRAGMA synchronous=FULL; PRAGMA journal_mode=WAL;", -- 2.52.0 From d96066e6250bb628c2692f34bda8666b975a886d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 12 May 2026 18:25:27 +0200 Subject: [PATCH 5/5] Re-enable tests --- .github/workflows/build-docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 7c8a47f..03dfc73 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -22,8 +22,8 @@ jobs: - name: Run Migrations run: uv run python manage.py migrate - # - name: Run Tests - # run: PROD=1 uv run pytest + - name: Run Tests + run: uv run --with pytest-django pytest build-and-push: needs: test -- 2.52.0