From 8b30105189320847396f51d91041c7bdd0628703 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Mon, 1 Dec 2025 08:24:15 +0100 Subject: [PATCH 01/16] feat(infra): add dockerfile, compose setup and migrate containerized deploy --- .dockerignore | 33 ++++++++ .env.example | 9 +- .github/workflows/ci.yml | 31 ++++++- Dockerfile | 48 +++++++++++ Makefile | 14 +++ ansible/Caddyfile | 19 +++-- ansible/ansible.cfg | 3 + ansible/deploy.yml | 179 ++++++++++++++++++--------------------- ansible/env.j2 | 9 +- config/settings.py | 1 + docker-compose.yml | 37 ++++++++ 11 files changed, 275 insertions(+), 108 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 ansible/ansible.cfg create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d1bcf0d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# Ignore VCS +.git +.gitignore + +# Python artifacts +__pycache__/ +*.py[cod] +*.pyc +*.pyo +*.pyd +*.so + +# Local env & tooling +.env +.env.* +venv/ +.virtualenv/ +.cache/ +.mypy_cache/ +.pytest_cache/ +.coverage + +# Django/SQLite development DB (use real DB in production) +db.sqlite3 + +# Node / misc +node_modules/ + +# Ansible deployment artifacts (not needed in runtime image) +ansible/ + +# Other +*.log diff --git a/.env.example b/.env.example index c3a7c55..858a8dd 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,8 @@ -DJANGO_DEBUG=True +DJANGO_SECRET_KEY=change-me +DJANGO_DEBUG=false +DJANGO_SETTINGS_MODULE=config.settings +DJANGO_STATIC_ROOT=/app/static/ +PORT=8000 +GUNICORN_WORKERS=3 +IMAGE_NAME=histrio/idontneedit:latest +SITE_DOMAIN=idontneedit.org.ru diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e749080..5f356d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,11 +38,40 @@ jobs: - name: Run tests run: uv run python manage.py test - deploy: + build-image: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + histrio/idontneedit:latest + histrio/idontneedit:${{ github.sha }} + cache-from: type=registry,ref=histrio/idontneedit:buildcache + cache-to: type=registry,ref=histrio/idontneedit:buildcache,mode=max + + deploy: + needs: build-image + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: - name: checkout uses: actions/checkout@v4 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4ee3320 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +ARG PYTHON_VERSION=3.13 +FROM python:${PYTHON_VERSION}-slim AS base + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl gosu \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin app \ + && mkdir -p /app /app/static \ + && chown -R app:app /app + +WORKDIR /app + +FROM base AS runtime + +COPY requirements.txt ./ +RUN pip install --upgrade pip wheel \ + && pip install -r requirements.txt --no-cache-dir + +ENV DJANGO_SETTINGS_MODULE=config.settings \ + DJANGO_STATIC_ROOT=/app/static/ \ + GUNICORN_WORKERS=3 \ + GUNICORN_BIND=0.0.0.0:8000 \ + GUNICORN_MAX_REQUESTS=1000 \ + GUNICORN_MAX_REQUESTS_JITTER=100 \ + PORT=8000 + + +COPY . /app + +RUN chown -R app:app /app + +USER app + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD curl -fsS "http://127.0.0.1:${PORT}/" || exit 1 + +CMD ["/bin/sh", "-c", "exec gunicorn config.wsgi:application --bind 0.0.0.0:${PORT:-8000} --workers ${GUNICORN_WORKERS:-3} --access-logfile - --error-logfile -"] diff --git a/Makefile b/Makefile index c4d2fef..0cba1e5 100644 --- a/Makefile +++ b/Makefile @@ -15,3 +15,17 @@ requirements.txt: uv.lock @test -r .env \ && echo "Your .env is older than .env.example" \ || cp .env.example .env + +DOCKER?=docker +IMAGE_REPO:=histrio/idontneedit +IMAGE_TAG?=$(shell git rev-parse --short HEAD) +LATEST_TAG?=latest + +.PHONY: image.build +image.build: + @$(DOCKER) build -t $(IMAGE_REPO):$(IMAGE_TAG) -t $(IMAGE_REPO):$(LATEST_TAG) . + +.PHONY: image.push +image.push: + @$(DOCKER) push $(IMAGE_REPO):$(IMAGE_TAG) + @$(DOCKER) push $(IMAGE_REPO):$(LATEST_TAG) diff --git a/ansible/Caddyfile b/ansible/Caddyfile index 4bba58e..5cd7c29 100644 --- a/ansible/Caddyfile +++ b/ansible/Caddyfile @@ -1,15 +1,18 @@ -idontneedit.org.ru { +{$SITE_DOMAIN} { + encode zstd gzip - - handle_path /static/* { - root * /srv/idontneedit/static - file_server + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" } - handle_path /media/* { - root * /srv/idontneedit/media + handle_path /static/* { + root * /app/static + header Cache-Control "public, max-age=31536000, immutable" file_server } - reverse_proxy unix//srv/idontneedit/run/gunicorn.sock + reverse_proxy app:8000 } diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..94f1126 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,3 @@ +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s +pipelining = True diff --git a/ansible/deploy.yml b/ansible/deploy.yml index da33ea3..e83e552 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -3,7 +3,6 @@ vars: dest: /srv/idontneedit/app - venv: /srv/idontneedit/venv ssh_port: 22 vars_files: @@ -99,135 +98,121 @@ state: present update_cache: yes - - name: install systemd unit - copy: - src: idontneedit.service - dest: /etc/systemd/system/idontneedit.service - owner: root - group: root - mode: 644 + - name: ensure project dirs + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: deploy + group: www-data + mode: '0755' + loop: + - "{{ dest }}" + - "{{ dest }}/ansible" + + - name: copy docker-compose.yml + ansible.builtin.copy: + src: "{{ playbook_dir }}/../docker-compose.yml" + dest: "{{ dest }}/docker-compose.yml" + owner: deploy + group: www-data + mode: '0644' + + - name: copy Caddyfile for proxy + ansible.builtin.copy: + src: "{{ playbook_dir }}/Caddyfile" + dest: "{{ dest }}/ansible/Caddyfile" + owner: deploy + group: www-data + mode: '0644' - - name: upload env file + - name: upload env file for Docker Compose ansible.builtin.template: src: env.j2 - dest: /srv/idontneedit/env + dest: "{{ dest }}/.env" owner: deploy group: deploy mode: "0600" become: yes - - name: reload systemd - command: systemctl daemon-reload - - - name: enable service - systemd: - name: idontneedit - enabled: yes - - - name: ensure dirs - file: - path: "{{ item }}" - state: directory - owner: deploy - group: www-data - mode: '0755' - loop: - - /srv/idontneedit/static - - /srv/idontneedit/media - - /srv/idontneedit/logs - - /srv/idontneedit/venv - - /srv/idontneedit/app - - /srv/idontneedit/run - - - name: copy application files - ansible.posix.synchronize: - src: "{{ playbook_dir }}/../" - dest: "{{ dest }}" - delete: yes - owner: no - group: no - rsync_opts: - - "--exclude=.git" - - "--exclude=__pycache__" - - "--exclude=*.pyc" - - "--exclude=.env" - - "--exclude=db.sqlite3" - - "--exclude=venv" - - "--exclude=.venv" - - "--exclude=ansible/" - - "--chown=deploy:www-data" - rsync_path: "rsync" - - - name: ensure database file exists with correct permissions + - name: ensure database file exists with permissive permissions (for container write) ansible.builtin.file: path: "{{ dest }}/db.sqlite3" owner: deploy group: www-data - mode: '0644' + mode: '0666' state: touch modification_time: preserve access_time: preserve - - name: ensure python3-venv is installed + - name: ensure prerequisites for Docker repo ansible.builtin.package: name: - - python3 - - python3-venv + - ca-certificates + - curl + - gnupg state: present update_cache: yes - - name: create venv - ansible.builtin.command: python3 -m venv "{{ venv }}" + - name: ensure /etc/apt/keyrings exists + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + + - name: add Docker APT GPG key (Ubuntu) + ansible.builtin.shell: "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg" args: - creates: "{{ venv }}/bin/activate" - become_user: deploy + creates: /etc/apt/keyrings/docker.gpg - - name: install dependencies - become_user: deploy - ansible.builtin.command: "{{ venv }}/bin/pip install -r requirements.txt" + - name: add Docker APT repository (Ubuntu) + ansible.builtin.apt_repository: + repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable" + filename: docker + state: present + + - name: install Docker engine and plugins from Docker repo + ansible.builtin.package: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + update_cache: yes + + - name: add deploy user to docker group + ansible.builtin.user: + name: deploy + groups: docker + append: yes + + - name: start and enable docker service + ansible.builtin.systemd: + name: docker + enabled: yes + state: started + + - name: pull latest images via Docker Compose + ansible.builtin.command: docker compose pull args: chdir: "{{ dest }}" + become_user: deploy - - name: run migrations - ansible.builtin.command: "{{ venv }}/bin/python manage.py migrate --noinput" + - name: bring up stack via Docker Compose (force recreate) + ansible.builtin.command: docker compose up -d --force-recreate args: chdir: "{{ dest }}" become_user: deploy - - name: collect static - become_user: deploy - ansible.builtin.command: "{{ venv }}/bin/python manage.py collectstatic --noinput" + - name: run database migrations + ansible.builtin.command: docker compose exec -T app python manage.py migrate --noinput args: chdir: "{{ dest }}" + become_user: deploy - - name: restart service - ansible.builtin.systemd: - name: idontneedit - state: restarted - - - name: install Caddy - apt: - name: caddy - state: present - update_cache: yes - - - name: install Caddyfile - copy: - src: Caddyfile - dest: /etc/caddy/Caddyfile - owner: root - group: root - mode: '0644' - backup: yes - - - name: enable and start caddy - ansible.builtin.systemd: - name: caddy - enabled: yes - state: restarted - - - name: Run deployment checks - ansible.builtin.command: "{{ venv }}/bin/python manage.py check --deploy --fail-level=WARNING" + - name: collect static files into shared volume + ansible.builtin.command: docker compose exec -T app python manage.py collectstatic --noinput args: chdir: "{{ dest }}" become_user: deploy diff --git a/ansible/env.j2 b/ansible/env.j2 index 71470fe..79a1aac 100644 --- a/ansible/env.j2 +++ b/ansible/env.j2 @@ -1 +1,8 @@ -SECRET_KEY={{ secret_key }} +DJANGO_SECRET_KEY={{ secret_key }} +DJANGO_DEBUG={{ django_debug | default('false') }} +DJANGO_SETTINGS_MODULE={{ django_settings_module | default('config.settings') }} +DJANGO_STATIC_ROOT={{ django_static_root | default('/app/static/') }} +PORT={{ port | default('8000') }} +GUNICORN_WORKERS={{ gunicorn_workers | default('3') }} +IMAGE_NAME={{ image_name | default('histrio/idontneedit:latest') }} +SITE_DOMAIN={{ site_domain | default('idontneedit.org.ru') }} diff --git a/config/settings.py b/config/settings.py index 25df8ea..03c8f33 100644 --- a/config/settings.py +++ b/config/settings.py @@ -127,6 +127,7 @@ CSRF_TRUSTED_ORIGINS = ["https://idontneedit.org.ru"] if not DEBUG: + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..83bef4d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +services: + app: + image: ${IMAGE_NAME:-histrio/idontneedit:latest} + pull_policy: always + ports: + - "8000:8000" + env_file: + - .env + volumes: + - ./db.sqlite3:/app/db.sqlite3 + - static-data:/app/static + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:${PORT:-8000}/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + restart: unless-stopped + + caddy: + image: caddy:2-alpine + depends_on: + app: + condition: service_started + ports: + - "80:80" + - "443:443" + volumes: + - ./ansible/Caddyfile:/etc/caddy/Caddyfile:ro + - static-data:/app/static:ro + env_file: + - .env + restart: unless-stopped + +volumes: + static-data: + driver: local From a9e986e773f2a3642dfb53a7e2a442b5f1d70791 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Mon, 1 Dec 2025 08:47:01 +0100 Subject: [PATCH 02/16] Update docker-compose.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 83bef4d..c8ced24 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - ./db.sqlite3:/app/db.sqlite3 - static-data:/app/static healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:${PORT:-8000}/"] + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/"] interval: 30s timeout: 5s retries: 3 From 06ead2347d935a52b2bdd7be2fe30910bbccd55a Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Mon, 1 Dec 2025 08:47:17 +0100 Subject: [PATCH 03/16] Update ansible/env.j2 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ansible/env.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/env.j2 b/ansible/env.j2 index 79a1aac..0bdd787 100644 --- a/ansible/env.j2 +++ b/ansible/env.j2 @@ -1,5 +1,5 @@ DJANGO_SECRET_KEY={{ secret_key }} -DJANGO_DEBUG={{ django_debug | default('false') }} +DJANGO_DEBUG={{ django_debug | default('False') }} DJANGO_SETTINGS_MODULE={{ django_settings_module | default('config.settings') }} DJANGO_STATIC_ROOT={{ django_static_root | default('/app/static/') }} PORT={{ port | default('8000') }} From 0e09a59d909f018f2ef6a6456904a0ae27801329 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Mon, 1 Dec 2025 09:03:31 +0100 Subject: [PATCH 04/16] feat(infra): introduce pg and migrate settings to django-environ --- .env.example | 5 ++++ ansible/env.j2 | 5 ++++ config/settings.py | 39 +++++++++++++++++++------- docker-compose.override.yml | 11 ++++++++ docker-compose.yml | 27 +++++++++++++++++- manage.py | 4 --- pyproject.toml | 3 +- requirements.txt | 34 +++++++++++++++++++--- uv.lock | 56 +++++++++++++++++++++++++++++++++---- 9 files changed, 158 insertions(+), 26 deletions(-) create mode 100644 docker-compose.override.yml diff --git a/.env.example b/.env.example index 858a8dd..370501c 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,8 @@ PORT=8000 GUNICORN_WORKERS=3 IMAGE_NAME=histrio/idontneedit:latest SITE_DOMAIN=idontneedit.org.ru +POSTGRES_DB=idontneedit +POSTGRES_USER=idontneedit +POSTGRES_PASSWORD=changeme +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 diff --git a/ansible/env.j2 b/ansible/env.j2 index 0bdd787..9efe42a 100644 --- a/ansible/env.j2 +++ b/ansible/env.j2 @@ -6,3 +6,8 @@ PORT={{ port | default('8000') }} GUNICORN_WORKERS={{ gunicorn_workers | default('3') }} IMAGE_NAME={{ image_name | default('histrio/idontneedit:latest') }} SITE_DOMAIN={{ site_domain | default('idontneedit.org.ru') }} +POSTGRES_DB={{ postgres_db | default('idontneedit') }} +POSTGRES_USER={{ postgres_user | default('idontneedit') }} +POSTGRES_PASSWORD={{ postgres_password | default('changeme') }} +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 diff --git a/config/settings.py b/config/settings.py index 03c8f33..604cb4b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -11,20 +11,25 @@ """ from pathlib import Path -import os +import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +# Initialize environ +env = environ.Env(DJANGO_DEBUG=(bool, False)) + +# Read .env file if it exists +environ.Env.read_env(BASE_DIR / ".env") # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ -SECRET_KEY = os.getenv( - "DJANGO_SECRET_KEY", "django-insecure-please-change-me-in-production" +SECRET_KEY = env( + "DJANGO_SECRET_KEY", default="django-insecure-please-change-me-in-production" ) -DEBUG = os.getenv("DJANGO_DEBUG", "False").lower() in {"1", "true", "yes"} +DEBUG = env.bool("DJANGO_DEBUG", default=False) ALLOWED_HOSTS = [ "idontneedit.org.ru", @@ -78,12 +83,26 @@ # Database # https://docs.djangoproject.com/en/dev/ref/settings/#databases -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", +if DEBUG: + # Use SQLite for development + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } +else: + # Use Postgres for production + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env("POSTGRES_DB", default="idontneedit"), + "USER": env("POSTGRES_USER", default="idontneedit"), + "PASSWORD": env("POSTGRES_PASSWORD"), + "HOST": env("POSTGRES_HOST", default="postgres"), + "PORT": env("POSTGRES_PORT", default="5432"), + } } -} # Password validation @@ -122,7 +141,7 @@ STATIC_URL = "static/" -STATIC_ROOT = os.getenv("DJANGO_STATIC_ROOT", "/srv/idontneedit/static/") +STATIC_ROOT = env("DJANGO_STATIC_ROOT", default="/app/static/") CSRF_TRUSTED_ORIGINS = ["https://idontneedit.org.ru"] diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..fcf3ec6 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,11 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + image: idontneedit:dev + volumes: + - .:/app + - static-data:/app/static + environment: + DJANGO_DEBUG: "true" diff --git a/docker-compose.yml b/docker-compose.yml index c8ced24..663aa8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,30 @@ services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-idontneedit} + POSTGRES_USER: ${POSTGRES_USER:-idontneedit} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-idontneedit}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + app: image: ${IMAGE_NAME:-histrio/idontneedit:latest} pull_policy: always + depends_on: + postgres: + condition: service_healthy ports: - "8000:8000" env_file: - .env volumes: - - ./db.sqlite3:/app/db.sqlite3 - static-data:/app/static healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/"] @@ -28,6 +45,8 @@ services: volumes: - ./ansible/Caddyfile:/etc/caddy/Caddyfile:ro - static-data:/app/static:ro + - caddy-data:/data + - caddy-config:/config env_file: - .env restart: unless-stopped @@ -35,3 +54,9 @@ services: volumes: static-data: driver: local + postgres-data: + driver: local + caddy-data: + driver: local + caddy-config: + driver: local diff --git a/manage.py b/manage.py index 4cbac55..d28672e 100755 --- a/manage.py +++ b/manage.py @@ -3,13 +3,9 @@ import os import sys -import dotenv - def main(): """Run administrative tasks.""" - dotenv.read_dotenv() - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") try: from django.core.management import execute_from_command_line diff --git a/pyproject.toml b/pyproject.toml index e27ccc4..b09bfa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,9 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "django==6.0rc1", - "django-dotenv>=1.4.2", + "django-environ>=0.11.2", "gunicorn>=23.0.0", + "psycopg[binary]>=3.2.3", ] [dependency-groups] diff --git a/requirements.txt b/requirements.txt index bdf1584..7289c64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,9 +35,9 @@ django==6.0rc1 \ --hash=sha256:28d47cddbb7ef9c39ad7441c72e6c2d47a487397780ae9e75a4774fe20ac1a7d \ --hash=sha256:d37fc9cf38a30a20634ca7bc18580cb86351b01e51eda4e06dc66ab9ffe2e7d8 # via idontneedit -django-dotenv==1.4.2 \ - --hash=sha256:3812bb0f4876cf31f902aad140f0645e120e51ee30eb7c40c22050f58a0e4adb \ - --hash=sha256:a9b1b40a70bd321acd231926acedb9bd2c5e873e33a1873b34a7276d196a765e +django-environ==0.12.0 \ + --hash=sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a \ + --hash=sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca # via idontneedit filelock==3.20.0 \ --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ @@ -78,6 +78,30 @@ platformdirs==4.5.0 \ pre-commit==4.5.0 \ --hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \ --hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b +psycopg==3.2.13 \ + --hash=sha256:309adaeda61d44556046ec9a83a93f42bbe5310120b1995f3af49ab6d9f13c1d \ + --hash=sha256:a481374514f2da627157f767a9336705ebefe93ea7a0522a6cbacba165da179a + # via idontneedit +psycopg-binary==3.2.13 ; implementation_name != 'pypy' \ + --hash=sha256:13e2f8894d410678529ff9f1211f96c5a93ff142f992b302682b42d924428b61 \ + --hash=sha256:2f63868cc96bc18486cebec24445affbdd7f7debf28fac466ea935a8b5a4753b \ + --hash=sha256:4a6cafabdc0bfa37e11c6f365020fd5916b62d6296df581f4dceaa43a2ce680c \ + --hash=sha256:502a778c3e07c6b3aabfa56ee230e8c264d2debfab42d11535513a01bdfff0d6 \ + --hash=sha256:594dfbca3326e997ae738d3d339004e8416b1f7390f52ce8dc2d692393e8fa96 \ + --hash=sha256:596176ae3dfbf56fc61108870bfe17c7205d33ac28d524909feb5335201daa0a \ + --hash=sha256:5c77f156c7316529ed371b5f95a51139e531328ee39c37493a2afcbc1f79d5de \ + --hash=sha256:65df0d459ffba14082d8ca4bb2f6ffbb2f8d02968f7d34a747e1031934b76b23 \ + --hash=sha256:7561a71d764d6f74d66e8b7d844b0f27fa33de508f65c17b1d56a94c73644776 \ + --hash=sha256:8b843c00478739e95c46d6d3472b13123b634685f107831a9bfc41503a06ecbd \ + --hash=sha256:9caf14745a1930b4e03fe4072cd7154eaf6e1241d20c42130ed784408a26b24b \ + --hash=sha256:ac92d6bc1d4a41c7459953a9aa727b9966e937e94c9e072527317fd2a67d488b \ + --hash=sha256:c96cb5a27e68acac6d74b64fca38592a692de9c4b7827339190698d58027aa45 \ + --hash=sha256:cc3a0408435dfbb77eeca5e8050df4b19a6e9b7e5e5583edf524c4a83d6293b2 \ + --hash=sha256:dbae6ab1966e2b61d97e47220556c330c4608bb4cfb3a124aa0595c39995c068 \ + --hash=sha256:ea2fdbcc9142933a47c66970e0df8b363e3bd1ea4c5ce376f2f3d94a9aeec847 \ + --hash=sha256:f26f7009375cf1e92180e5c517c52da1054f7e690dde90e0ed00fa8b5736bcd4 \ + --hash=sha256:fae933e4564386199fc54845d85413eedb49760e0bcd2b621fde2dd1825b99b3 + # via psycopg pytokens==0.3.0 \ --hash=sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a \ --hash=sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3 @@ -120,7 +144,9 @@ sqlparse==0.5.4 \ tzdata==2025.2 ; sys_platform == 'win32' \ --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via django + # via + # django + # psycopg virtualenv==20.35.4 \ --hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \ --hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b diff --git a/uv.lock b/uv.lock index b5e58eb..cd3e546 100644 --- a/uv.lock +++ b/uv.lock @@ -90,12 +90,12 @@ wheels = [ ] [[package]] -name = "django-dotenv" -version = "1.4.2" +name = "django-environ" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/18/b1d0bb605a5737e9a62c5135cf4bc234a5186abfbe1b06706ea68f235f32/django-dotenv-1.4.2.tar.gz", hash = "sha256:3812bb0f4876cf31f902aad140f0645e120e51ee30eb7c40c22050f58a0e4adb", size = 4295, upload-time = "2017-12-11T16:44:36.177Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804, upload-time = "2025-01-13T17:03:37.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/af/d9175f2b40a9bcd700db2861610d6ed48c2795ffba46c1d6abf25f7c1dea/django_dotenv-1.4.2-py2.py3-none-any.whl", hash = "sha256:a9b1b40a70bd321acd231926acedb9bd2c5e873e33a1873b34a7276d196a765e", size = 3794, upload-time = "2017-12-11T16:44:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957, upload-time = "2025-01-13T17:03:32.918Z" }, ] [[package]] @@ -134,8 +134,9 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "django" }, - { name = "django-dotenv" }, + { name = "django-environ" }, { name = "gunicorn" }, + { name = "psycopg", extra = ["binary"] }, ] [package.dev-dependencies] @@ -147,8 +148,9 @@ dev = [ [package.metadata] requires-dist = [ { name = "django", specifier = "==6.0rc1" }, - { name = "django-dotenv", specifier = ">=1.4.2" }, + { name = "django-environ", specifier = ">=0.11.2" }, { name = "gunicorn", specifier = ">=23.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2.3" }, ] [package.metadata.requires-dev] @@ -218,6 +220,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, ] +[[package]] +name = "psycopg" +version = "3.2.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/05/d4a05988f15fcf90e0088c735b1f2fc04a30b7fc65461d6ec278f5f2f17a/psycopg-3.2.13.tar.gz", hash = "sha256:309adaeda61d44556046ec9a83a93f42bbe5310120b1995f3af49ab6d9f13c1d", size = 160626, upload-time = "2025-11-21T22:34:32.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/14/f2724bd1986158a348316e86fdd0837a838b14a711df3f00e47fba597447/psycopg-3.2.13-py3-none-any.whl", hash = "sha256:a481374514f2da627157f767a9336705ebefe93ea7a0522a6cbacba165da179a", size = 206797, upload-time = "2025-11-21T22:29:39.733Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.13" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/ec/ef37bb44dc02fcc6c0a3eeb93f4baaac13bcb228633fe38ad3fb5a3f6449/psycopg_binary-3.2.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbae6ab1966e2b61d97e47220556c330c4608bb4cfb3a124aa0595c39995c068", size = 3995628, upload-time = "2025-11-21T22:31:45.921Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ad/4748f5f1a40248af16dba087dbec50bd335ee025cc1fb9bf64773378ceff/psycopg_binary-3.2.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fae933e4564386199fc54845d85413eedb49760e0bcd2b621fde2dd1825b99b3", size = 4069024, upload-time = "2025-11-21T22:31:50.202Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/f02ec6bbc30c7fcd3b39823d2d624b42fae480edeb6e50eb3276281d5635/psycopg_binary-3.2.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:13e2f8894d410678529ff9f1211f96c5a93ff142f992b302682b42d924428b61", size = 4615127, upload-time = "2025-11-21T22:31:56.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0d/a54fc2cdd672c84175d6869cc823d6ec2a8909318d491f3c24e6077983f2/psycopg_binary-3.2.13-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f26f7009375cf1e92180e5c517c52da1054f7e690dde90e0ed00fa8b5736bcd4", size = 4710267, upload-time = "2025-11-21T22:32:04.585Z" }, + { url = "https://files.pythonhosted.org/packages/9d/b7/067de1acaf3d312253351f3af4121f972584bd36cada6378d4b0cdcebd38/psycopg_binary-3.2.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea2fdbcc9142933a47c66970e0df8b363e3bd1ea4c5ce376f2f3d94a9aeec847", size = 4400795, upload-time = "2025-11-21T22:32:08.883Z" }, + { url = "https://files.pythonhosted.org/packages/64/b5/030e6b1ebfc4d3a8fca03adc5fc827982643bad0b01a1268538d17c08ed3/psycopg_binary-3.2.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac92d6bc1d4a41c7459953a9aa727b9966e937e94c9e072527317fd2a67d488b", size = 3851239, upload-time = "2025-11-21T22:32:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/0541845364a7de9eae6807060da6a04b22a8eb2e803606d285d9250fbe93/psycopg_binary-3.2.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8b843c00478739e95c46d6d3472b13123b634685f107831a9bfc41503a06ecbd", size = 3525084, upload-time = "2025-11-21T22:32:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/83/ae/6507890dc30a4bbd9d938d4ff3a4079d009a5ad8170af51c7f762438fdbf/psycopg_binary-3.2.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f63868cc96bc18486cebec24445affbdd7f7debf28fac466ea935a8b5a4753b", size = 3576787, upload-time = "2025-11-21T22:32:19.922Z" }, + { url = "https://files.pythonhosted.org/packages/9d/64/3d1c2f1fd09b60cdfbe68b9a810b357ba505eff6e4bdb1a2d9f6729da64c/psycopg_binary-3.2.13-cp313-cp313-win_amd64.whl", hash = "sha256:594dfbca3326e997ae738d3d339004e8416b1f7390f52ce8dc2d692393e8fa96", size = 2905584, upload-time = "2025-11-21T22:32:23.399Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b4/7656b3d67bedff2b900c8c4671cb6eb5fb99c2fc36da33579cac89779c25/psycopg_binary-3.2.13-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:502a778c3e07c6b3aabfa56ee230e8c264d2debfab42d11535513a01bdfff0d6", size = 3997201, upload-time = "2025-11-21T22:32:28.185Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2e/3b4afbd94d48df19c3931cedba464b109f89d81ac43178e6a3d654b4e8d5/psycopg_binary-3.2.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7561a71d764d6f74d66e8b7d844b0f27fa33de508f65c17b1d56a94c73644776", size = 4071631, upload-time = "2025-11-21T22:32:32.594Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/107d06d55992e2f13157eb705ba5a47d06c4cf1bed077dff0c567b10c187/psycopg_binary-3.2.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9caf14745a1930b4e03fe4072cd7154eaf6e1241d20c42130ed784408a26b24b", size = 4620918, upload-time = "2025-11-21T22:32:37.357Z" }, + { url = "https://files.pythonhosted.org/packages/e1/47/a925620f261b115f31e813a5bfe640f316413b1864094a60162f4a6e4d67/psycopg_binary-3.2.13-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a6cafabdc0bfa37e11c6f365020fd5916b62d6296df581f4dceaa43a2ce680c", size = 4714494, upload-time = "2025-11-21T22:32:42.138Z" }, + { url = "https://files.pythonhosted.org/packages/46/33/bed384665356bb9ba17dd8e104884d87cc2343d16dffdfd9aaa9a159bd4d/psycopg_binary-3.2.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96cb5a27e68acac6d74b64fca38592a692de9c4b7827339190698d58027aa45", size = 4403046, upload-time = "2025-11-21T22:32:47.241Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/749d8e8102fb5df502e2ecb053b79e78e3358af01af652b5dbeb96ab7905/psycopg_binary-3.2.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:596176ae3dfbf56fc61108870bfe17c7205d33ac28d524909feb5335201daa0a", size = 3859046, upload-time = "2025-11-21T22:32:51.481Z" }, + { url = "https://files.pythonhosted.org/packages/38/7c/f492e63b517d6dcd564e8c43bc15e11a4c712a848adf8938ce33bfd4c867/psycopg_binary-3.2.13-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:cc3a0408435dfbb77eeca5e8050df4b19a6e9b7e5e5583edf524c4a83d6293b2", size = 3531351, upload-time = "2025-11-21T22:32:55.571Z" }, + { url = "https://files.pythonhosted.org/packages/07/5a/d8743eb23944e5cf2a0bbfa92935c140b5beaacdb872be641065ed70ab2c/psycopg_binary-3.2.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65df0d459ffba14082d8ca4bb2f6ffbb2f8d02968f7d34a747e1031934b76b23", size = 3581034, upload-time = "2025-11-21T22:33:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/46/b2/411d4180252144f7eff024894d2d2ebb98c012c944a282fc20250870e461/psycopg_binary-3.2.13-cp314-cp314-win_amd64.whl", hash = "sha256:5c77f156c7316529ed371b5f95a51139e531328ee39c37493a2afcbc1f79d5de", size = 3000162, upload-time = "2025-11-21T22:33:07.378Z" }, +] + [[package]] name = "pytokens" version = "0.3.0" From 0c87ae375c9fb4922d755b93d49e2a09a6fbcc23 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 10:33:56 +0100 Subject: [PATCH 05/16] chore(precommit): more safety hooks and forbid .env files --- .pre-commit-config.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d188fad..040ff9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,12 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - id: check-added-large-files + - id: forbid-new-submodules + - id: no-commit-to-branch + args: ['--branch', 'main', '--branch', 'master'] + - id: check-case-conflict + - id: check-merge-conflict + - id: detect-private-key - repo: https://github.com/psf/black rev: 25.11.0 hooks: @@ -20,3 +26,11 @@ repos: rev: v1.4.0 hooks: - id: detect-secrets + +- repo: local + hooks: + - id: forbid-env-files + name: Forbid .env files + entry: '.env files must not be committed' + language: fail + files: '(^|/)\.env($|\.(?!example$).*)' From bb8e4a080504de80de1c5cca5b45059f281f799c Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 10:34:05 +0100 Subject: [PATCH 06/16] refactor(settings): load .env only when DEBUG is true --- .github/workflows/ci.yml | 1 - config/settings.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f356d5..4bb2320 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,6 @@ jobs: - name: Install uv run: pip install uv - - name: Install project run: uv sync diff --git a/config/settings.py b/config/settings.py index 604cb4b..f78f8da 100644 --- a/config/settings.py +++ b/config/settings.py @@ -19,8 +19,10 @@ # Initialize environ env = environ.Env(DJANGO_DEBUG=(bool, False)) -# Read .env file if it exists -environ.Env.read_env(BASE_DIR / ".env") +# Only read .env file in development +DEBUG = env.bool("DJANGO_DEBUG", default=False) +if DEBUG: + environ.Env.read_env(BASE_DIR / ".env") # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ @@ -29,8 +31,6 @@ "DJANGO_SECRET_KEY", default="django-insecure-please-change-me-in-production" ) -DEBUG = env.bool("DJANGO_DEBUG", default=False) - ALLOWED_HOSTS = [ "idontneedit.org.ru", "www.idontneedit.org.ru", @@ -98,7 +98,7 @@ "ENGINE": "django.db.backends.postgresql", "NAME": env("POSTGRES_DB", default="idontneedit"), "USER": env("POSTGRES_USER", default="idontneedit"), - "PASSWORD": env("POSTGRES_PASSWORD"), + "PASSWORD": env("POSTGRES_PASSWORD", default=""), "HOST": env("POSTGRES_HOST", default="postgres"), "PORT": env("POSTGRES_PORT", default="5432"), } From 0ea46890e862f6e72b68d92bb7ce755f49af7f73 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 11:53:10 +0100 Subject: [PATCH 07/16] Update Dockerfile Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4ee3320..1a94355 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ RUN apt-get update \ && apt-get install -y --no-install-recommends \ - ca-certificates curl gosu \ + ca-certificates curl \ && rm -rf /var/lib/apt/lists/* RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin app \ From 874453b1918da5656c874fce2240c69ad6fc9e97 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 11:55:21 +0100 Subject: [PATCH 08/16] Update docker-compose.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 663aa8f..50bf68f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,7 @@ services: volumes: - static-data:/app/static healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/"] + test: ["CMD", "curl", "-fsSL", "--fail", "http://127.0.0.1:8000/health/"] interval: 30s timeout: 5s retries: 3 From 7a62da582b758227f1bfbe478ded44ddf3ad0431 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 11:56:08 +0100 Subject: [PATCH 09/16] Update ansible/env.j2 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ansible/env.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/env.j2 b/ansible/env.j2 index 9efe42a..b9f931a 100644 --- a/ansible/env.j2 +++ b/ansible/env.j2 @@ -8,6 +8,6 @@ IMAGE_NAME={{ image_name | default('histrio/idontneedit:latest') }} SITE_DOMAIN={{ site_domain | default('idontneedit.org.ru') }} POSTGRES_DB={{ postgres_db | default('idontneedit') }} POSTGRES_USER={{ postgres_user | default('idontneedit') }} -POSTGRES_PASSWORD={{ postgres_password | default('changeme') }} +POSTGRES_PASSWORD={{ postgres_password }} POSTGRES_HOST=postgres POSTGRES_PORT=5432 From 42ae96ec5ec9fd6021cadba279ac87ebdfdfd330 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 12:26:02 +0100 Subject: [PATCH 10/16] chore(ansible): remove obsolete SQLite setup and systemd unit --- ansible/deploy.yml | 10 ---------- ansible/idontneedit.service | 24 ------------------------ 2 files changed, 34 deletions(-) delete mode 100644 ansible/idontneedit.service diff --git a/ansible/deploy.yml b/ansible/deploy.yml index e83e552..e6e3ff6 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -134,16 +134,6 @@ mode: "0600" become: yes - - name: ensure database file exists with permissive permissions (for container write) - ansible.builtin.file: - path: "{{ dest }}/db.sqlite3" - owner: deploy - group: www-data - mode: '0666' - state: touch - modification_time: preserve - access_time: preserve - - name: ensure prerequisites for Docker repo ansible.builtin.package: name: diff --git a/ansible/idontneedit.service b/ansible/idontneedit.service deleted file mode 100644 index 69914c0..0000000 --- a/ansible/idontneedit.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=idontneedit web app -After=network.target - -[Service] -WorkingDirectory=/srv/idontneedit/app - -ExecStart=/srv/idontneedit/venv/bin/gunicorn config.wsgi:application --bind unix:/srv/idontneedit/run/gunicorn.sock --workers 3 - -User=deploy -Group=www-data - -RuntimeDirectory=idontneedit -RuntimeDirectoryMode=0755 - -Restart=always -RestartSec=3 - -Environment=DJANGO_SETTINGS_MODULE=config.settings -Environment=PYTHONUNBUFFERED=1 -EnvironmentFile=-/srv/idontneedit/env - -[Install] -WantedBy=multi-user.target From 36badfa50cc6be5503cc5394bcfaad1eda5e0034 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 12:26:20 +0100 Subject: [PATCH 11/16] chore(docker): set production env and remove legacy debug/env overrides --- ansible/env.j2 | 1 + config/settings.py | 41 ++++++++++++++++++++++--------------- docker-compose.override.yml | 2 -- docker-compose.yml | 4 ++-- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/ansible/env.j2 b/ansible/env.j2 index 9efe42a..dcb527a 100644 --- a/ansible/env.j2 +++ b/ansible/env.j2 @@ -1,3 +1,4 @@ +DJANGO_PRODUCTION=true DJANGO_SECRET_KEY={{ secret_key }} DJANGO_DEBUG={{ django_debug | default('False') }} DJANGO_SETTINGS_MODULE={{ django_settings_module | default('config.settings') }} diff --git a/config/settings.py b/config/settings.py index f78f8da..32eed9b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -16,20 +16,31 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -# Initialize environ -env = environ.Env(DJANGO_DEBUG=(bool, False)) +env = environ.Env( + DJANGO_PRODUCTION=(bool, False), + DJANGO_DEBUG=(bool, False), + DJANGO_SECRET_KEY=(str, "django-insecure-please-change-me-in-production"), + POSTGRES_DB=(str, "idontneedit"), + POSTGRES_USER=(str, "idontneedit"), + POSTGRES_PASSWORD=(str, ""), + POSTGRES_HOST=(str, "postgres"), + POSTGRES_PORT=(str, "5432"), + DJANGO_STATIC_ROOT=(str, "/app/static/"), +) -# Only read .env file in development -DEBUG = env.bool("DJANGO_DEBUG", default=False) -if DEBUG: +PRODUCTION = env.bool("DJANGO_PRODUCTION") +if not PRODUCTION: environ.Env.read_env(BASE_DIR / ".env") +if PRODUCTION: + DEBUG = env.bool("DJANGO_DEBUG") +else: + DEBUG = True + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ -SECRET_KEY = env( - "DJANGO_SECRET_KEY", default="django-insecure-please-change-me-in-production" -) +SECRET_KEY = env.str("DJANGO_SECRET_KEY") ALLOWED_HOSTS = [ "idontneedit.org.ru", @@ -84,7 +95,6 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#databases if DEBUG: - # Use SQLite for development DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", @@ -92,15 +102,14 @@ } } else: - # Use Postgres for production DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": env("POSTGRES_DB", default="idontneedit"), - "USER": env("POSTGRES_USER", default="idontneedit"), - "PASSWORD": env("POSTGRES_PASSWORD", default=""), - "HOST": env("POSTGRES_HOST", default="postgres"), - "PORT": env("POSTGRES_PORT", default="5432"), + "NAME": env.str("POSTGRES_DB"), + "USER": env.str("POSTGRES_USER"), + "PASSWORD": env.str("POSTGRES_PASSWORD"), + "HOST": env.str("POSTGRES_HOST"), + "PORT": env.str("POSTGRES_PORT"), } } @@ -141,7 +150,7 @@ STATIC_URL = "static/" -STATIC_ROOT = env("DJANGO_STATIC_ROOT", default="/app/static/") +STATIC_ROOT = env.str("DJANGO_STATIC_ROOT") CSRF_TRUSTED_ORIGINS = ["https://idontneedit.org.ru"] diff --git a/docker-compose.override.yml b/docker-compose.override.yml index fcf3ec6..c06a156 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -7,5 +7,3 @@ services: volumes: - .:/app - static-data:/app/static - environment: - DJANGO_DEBUG: "true" diff --git a/docker-compose.yml b/docker-compose.yml index 663aa8f..9185617 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,8 @@ services: - "8000:8000" env_file: - .env + environment: + DJANGO_PRODUCTION: "true" volumes: - static-data:/app/static healthcheck: @@ -47,8 +49,6 @@ services: - static-data:/app/static:ro - caddy-data:/data - caddy-config:/config - env_file: - - .env restart: unless-stopped volumes: From 2b12b3bb4f0ca5d9dfe71fa9ad8e87009e556020 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 13:39:58 +0100 Subject: [PATCH 12/16] Update docker-compose.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 416091d..4fc9985 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,8 @@ services: - static-data:/app/static:ro - caddy-data:/data - caddy-config:/config + env_file: + - .env restart: unless-stopped volumes: From b19be04b4147bde0c9589f18d5acc6ee849dfccb Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 13:42:11 +0100 Subject: [PATCH 13/16] Update docker-compose.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4fc9985..d8f34e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,8 +20,6 @@ services: depends_on: postgres: condition: service_healthy - ports: - - "8000:8000" env_file: - .env environment: From a28398e9458f633faf2a68db73b606277d71d207 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 13:43:42 +0100 Subject: [PATCH 14/16] Update docker-compose.override.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docker-compose.override.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index c06a156..2d5c716 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -7,3 +7,5 @@ services: volumes: - .:/app - static-data:/app/static + - /app/.venv + - /app/__pycache__ From 8612660a8bf1cdf1c7ca8e49b0aa2f1de8e3bf83 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 14:41:58 +0100 Subject: [PATCH 15/16] chore(docker): update healthcheck and add caddy dev overrides --- docker-compose.override.yml | 3 +++ docker-compose.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 2d5c716..6235b8b 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -9,3 +9,6 @@ services: - static-data:/app/static - /app/.venv - /app/__pycache__ + caddy: + environment: + SITE_DOMAIN: localhost diff --git a/docker-compose.yml b/docker-compose.yml index d8f34e3..8a962bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,7 @@ services: volumes: - static-data:/app/static healthcheck: - test: ["CMD", "curl", "-fsSL", "--fail", "http://127.0.0.1:8000/health/"] + test: ["CMD", "curl", "-fsSL", "http://127.0.0.1:8000/"] interval: 30s timeout: 5s retries: 3 From b6ba86dd1f52e5439d51e8610aefa39d786cb9d9 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Tue, 2 Dec 2025 14:46:00 +0100 Subject: [PATCH 16/16] feat(ansible): create django superuser during deploy --- ansible/deploy.yml | 13 +++++++++++++ ansible/env.j2 | 3 +++ ansible/secrets.yml | 20 ++++++++++++-------- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/ansible/deploy.yml b/ansible/deploy.yml index e6e3ff6..4041775 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -206,3 +206,16 @@ args: chdir: "{{ dest }}" become_user: deploy + + - name: create superuser if it does not exist + ansible.builtin.command: docker compose exec -T app python manage.py createsuperuser --noinput + args: + chdir: "{{ dest }}" + become_user: deploy + environment: + DJANGO_SUPERUSER_USERNAME: "{{ django_superuser_username | default('admin') }}" + DJANGO_SUPERUSER_EMAIL: "{{ django_superuser_email | default('admin@example.com') }}" + DJANGO_SUPERUSER_PASSWORD: "{{ django_superuser_password | default('admin') }}" + register: superuser_result + failed_when: superuser_result.rc != 0 and 'already exists' not in superuser_result.stderr + changed_when: superuser_result.rc == 0 diff --git a/ansible/env.j2 b/ansible/env.j2 index b5b56e7..abc6ef2 100644 --- a/ansible/env.j2 +++ b/ansible/env.j2 @@ -12,3 +12,6 @@ POSTGRES_USER={{ postgres_user | default('idontneedit') }} POSTGRES_PASSWORD={{ postgres_password }} POSTGRES_HOST=postgres POSTGRES_PORT=5432 +DJANGO_SUPERUSER_USERNAME={{ django_superuser_username | default('admin') }} +DJANGO_SUPERUSER_EMAIL={{ django_superuser_email | default('admin@idontneedit.org.ru') }} +DJANGO_SUPERUSER_PASSWORD={{ django_superuser_password }} diff --git a/ansible/secrets.yml b/ansible/secrets.yml index 02f848d..68180ef 100644 --- a/ansible/secrets.yml +++ b/ansible/secrets.yml @@ -1,9 +1,13 @@ $ANSIBLE_VAULT;1.1;AES256 -30613238653936646365643862313738353663356631303936323638366330386137626465346439 -3331626264333062636463393834386336616561326264610a323330333638663436356637393464 -61303731623831326539663433316562356634376165623932633364343833646561333964353434 -3033376130386239360a613362393532633838373763646163326239343238366331303230386634 -34376563643537303663623332643133616662616435373063306465376633333033393331306364 -64373466663531663830346136316365633730333436613139663630393739313365313338613638 -31623035646564326232363962633234376365323634323039666531303837376431613062333238 -62396634313766643734 +30333336633261626235393465623166376266313464663163343231636135383361306365653133 +6633626335633965383934336336346230336130363936340a343438306362373163396265386536 +62396539316138343961623935373534643733383065396139383732396464356531333434633862 +3230343862306461310a663333613138363566653162626237363535346165616436353838303562 +39396236366235643064393637373437623435613433356139663839303038363736333262633230 +39383863663835323938613438363134343162656563333932383661613139333839626231373538 +61643962323137333138363334666364353263663664326535383437336631653265643164343539 +38386234376239653931323963343736383036656330386363303033623134316466303834316561 +62376632353136316138663362373566643466373537366266663565336431366465393633383034 +65643637356361356361323934663463373635666562663335336161623564616534333232383230 +30656134306236356365656662363761636363313037343964326631653964613464303932633034 +39323834633631333930