Skip to content

Conversation

@histrio
Copy link
Contributor

@histrio histrio commented Dec 1, 2025

…deploy

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the deployment infrastructure from a traditional systemd-based setup to a containerized Docker Compose deployment. The changes modernize the infrastructure by packaging the Django application in Docker, using Caddy as a containerized reverse proxy, and simplifying the deployment process through Docker Compose orchestration.

Key changes include:

  • Added Dockerfile with multi-stage build for the Django application running on Gunicorn
  • Created docker-compose.yml defining app and Caddy services with shared static volume
  • Updated Ansible playbook to deploy via Docker Compose instead of systemd service

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
Dockerfile Multi-stage build configuration for Python 3.13 Django app with Gunicorn, healthcheck, and non-root user
docker-compose.yml Service definitions for Django app and Caddy reverse proxy with volume mounts for SQLite DB and static files
config/settings.py Added SECURE_PROXY_SSL_HEADER for HTTPS handling behind reverse proxy
ansible/deploy.yml Replaced systemd/venv deployment with Docker installation and Docker Compose orchestration
ansible/env.j2 Updated environment template with Docker/Django configuration variables
ansible/Caddyfile Updated to use containerized paths and environment variable for domain
Makefile Added Docker image build and push targets
.env.example Updated with all required environment variables for containerized deployment
.dockerignore Standard Python/Django exclusions for Docker build context
ansible/ansible.cfg Added SSH connection optimization settings

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

env_file:
- .env
volumes:
- ./db.sqlite3:/app/db.sqlite3
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mounting SQLite database file directly from host can cause database corruption issues in containerized environments due to file locking problems. Consider using a Docker volume instead: - db-data:/app with a named volume, or migrate to a proper client-server database like PostgreSQL for production use.

Copilot uses AI. Check for mistakes.
owner: deploy
group: www-data
mode: '0644'
mode: '0666'
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting database file permissions to 0666 (world-writable) is a security risk. This allows any user on the system to read and modify the database. If the container requires write access, consider using a named volume with appropriate Docker volume permissions or ensure the container user ID matches the host user ID.

Copilot uses AI. Check for mistakes.
Comment on lines 162 to 165
- 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
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using shell with piped commands is less idiomatic than Ansible's dedicated modules. Consider using ansible.builtin.get_url to download the GPG key file and then ansible.builtin.command to dearmor it separately, which provides better error handling and idempotency.

Copilot uses AI. Check for mistakes.
Comment on lines +202 to +203
- name: bring up stack via Docker Compose (force recreate)
ansible.builtin.command: docker compose up -d --force-recreate
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using --force-recreate on every deployment will cause unnecessary downtime. Consider removing this flag and letting Docker Compose handle updates intelligently based on image/configuration changes, or only force recreate when truly needed.

Suggested change
- name: bring up stack via Docker Compose (force recreate)
ansible.builtin.command: docker compose up -d --force-recreate
- name: bring up stack via Docker Compose
ansible.builtin.command: docker compose up -d

Copilot uses AI. Check for mistakes.
ca-certificates curl gosu \
&& rm -rf /var/lib/apt/lists/*

RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin app \
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Hardcoded UID 10001 may conflict with existing UIDs on the host system when mounting volumes. Consider making the UID configurable via build argument or document why this specific UID was chosen.

Copilot uses AI. Check for mistakes.
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 -"]
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The CMD uses shell expansion for environment variables which is correct, but this could be simplified using the default environment values already set on lines 30-31. Consider referencing $GUNICORN_BIND directly or remove the defaults here to avoid duplication with ENV declarations.

Suggested change
CMD ["/bin/sh", "-c", "exec gunicorn config.wsgi:application --bind 0.0.0.0:${PORT:-8000} --workers ${GUNICORN_WORKERS:-3} --access-logfile - --error-logfile -"]
CMD ["/bin/sh", "-c", "exec gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers $GUNICORN_WORKERS --access-logfile - --error-logfile -"]

Copilot uses AI. Check for mistakes.
@histrio histrio force-pushed the feat/infra-add-dockerfile-compose-containerized-deploy branch from 7ad80fa to 8b30105 Compare December 1, 2025 07:39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated 14 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +31 to +34
GUNICORN_BIND=0.0.0.0:8000 \
GUNICORN_MAX_REQUESTS=1000 \
GUNICORN_MAX_REQUESTS_JITTER=100 \
PORT=8000
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variables GUNICORN_BIND and GUNICORN_MAX_REQUESTS are defined but never used in the CMD directive. The CMD at line 48 only uses PORT and GUNICORN_WORKERS. Either use these variables in the command or remove them:

CMD ["/bin/sh", "-c", "exec gunicorn config.wsgi:application --bind ${GUNICORN_BIND:-0.0.0.0:8000} --workers ${GUNICORN_WORKERS:-3} --max-requests ${GUNICORN_MAX_REQUESTS:-1000} --max-requests-jitter ${GUNICORN_MAX_REQUESTS_JITTER:-100} --access-logfile - --error-logfile -"]

Copilot uses AI. Check for mistakes.
Comment on lines 208 to 218
- 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
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running docker compose exec immediately after docker compose up -d may fail if the containers are not fully ready. While the postgres service has a healthcheck, the app service only has service_started condition from caddy's perspective. The migrations and collectstatic commands should either:

  1. Add a retry mechanism
  2. Wait for the app container to be healthy (it has a healthcheck defined)
  3. Add a delay or explicit readiness check

Consider using docker compose exec with retry logic or checking container health first.

Copilot uses AI. Check for mistakes.
@@ -1 +1,13 @@
DJANGO_DEBUG=True
DJANGO_SECRET_KEY=change-me
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The default value for DJANGO_DEBUG is inconsistent between files. In .env.example it's false (string), but django-environ expects a boolean. While django-environ can parse string values like "false", "False", "0", etc., using "true" or "false" strings is clearer. However, in config/settings.py line 23, env.bool() is used which properly handles this. Consider documenting that this should be "true" or "false" (lowercase) for consistency.

Suggested change
DJANGO_SECRET_KEY=change-me
DJANGO_SECRET_KEY=change-me
# Set DJANGO_DEBUG to "true" or "false" (lowercase) for clarity.

Copilot uses AI. Check for mistakes.
Comment on lines 137 to 145
- 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
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a SQLite database file in production when using PostgreSQL is unnecessary and confusing. The deployment is containerized with PostgreSQL, so this SQLite file creation should be removed. According to the settings, production uses PostgreSQL (lines 94-105 in config/settings.py), making this SQLite file obsolete.

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +169
- 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"
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Docker repository setup assumes the system is Ubuntu and uses a hardcoded arch=amd64. This will fail on ARM-based systems or non-Ubuntu Debian distributions. Consider detecting the architecture dynamically:

- name: detect system architecture
  ansible.builtin.command: dpkg --print-architecture
  register: system_arch
  changed_when: false

- name: add Docker APT repository
  ansible.builtin.apt_repository:
    repo: "deb [arch={{ system_arch.stdout }} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable"
Suggested change
- 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"
- name: detect system architecture
ansible.builtin.command: dpkg --print-architecture
register: system_arch
changed_when: false
- name: add Docker APT repository (Ubuntu)
ansible.builtin.apt_repository:
repo: "deb [arch={{ system_arch.stdout }} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable"

Copilot uses AI. Check for mistakes.
Comment on lines 21 to 25

# Only read .env file in development
DEBUG = env.bool("DJANGO_DEBUG", default=False)
if DEBUG:
environ.Env.read_env(BASE_DIR / ".env")
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .env file reading logic has a circular dependency issue. DEBUG is read from the environment at line 23, but the .env file is only loaded at line 25 if DEBUG is True. This means in production, if DJANGO_DEBUG is not already set in the environment before Django starts, it will default to False and never read the .env file.

The correct approach would be to either:

  1. Always read the .env file first, then check DEBUG, OR
  2. Remove conditional reading entirely and rely on environment variables being set externally in production

Consider changing to:

env = environ.Env(DJANGO_DEBUG=(bool, False))
# Read .env file if it exists (optional for development)
environ.Env.read_env(BASE_DIR / ".env", overwrite=False)
DEBUG = env.bool("DJANGO_DEBUG", default=False)
Suggested change
# Only read .env file in development
DEBUG = env.bool("DJANGO_DEBUG", default=False)
if DEBUG:
environ.Env.read_env(BASE_DIR / ".env")
# Read .env file if it exists (optional for development)
environ.Env.read_env(BASE_DIR / ".env", overwrite=False)
DEBUG = env.bool("DJANGO_DEBUG", default=False)

Copilot uses AI. Check for mistakes.
"ENGINE": "django.db.backends.postgresql",
"NAME": env("POSTGRES_DB", default="idontneedit"),
"USER": env("POSTGRES_USER", default="idontneedit"),
"PASSWORD": env("POSTGRES_PASSWORD"),
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production mode, POSTGRES_PASSWORD is required but has no default value. If this environment variable is not set, the application will crash when accessing the database configuration. Consider adding a default value or implementing explicit validation that raises a clear error message early in the application startup.

Example:

"PASSWORD": env("POSTGRES_PASSWORD", default=""),  # Or raise ImproperlyConfigured if empty

Copilot uses AI. Check for mistakes.
@histrio histrio force-pushed the feat/infra-add-dockerfile-compose-containerized-deploy branch from 62d51fd to bb8e4a0 Compare December 2, 2025 10:19
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 18 changed files in this pull request and generated 19 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +31
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)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image.build and image.push targets use IMAGE_TAG?=$(shell git rev-parse --short HEAD) which will fail if the directory is not a git repository or git is not installed. Consider adding error handling or documenting that these targets require git.

Suggested change
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)
# NOTE: The image.build and image.push targets require git to be installed and the directory to be a git repository.
IMAGE_TAG?=$(shell git rev-parse --short HEAD)
LATEST_TAG?=latest
.PHONY: image.build
image.build:
@if ! command -v git >/dev/null 2>&1; then \
echo "Error: git is not installed. Cannot determine IMAGE_TAG."; exit 1; \
fi; \
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then \
echo "Error: Not a git repository. Cannot determine IMAGE_TAG."; exit 1; \
fi; \
$(DOCKER) build -t $(IMAGE_REPO):$(IMAGE_TAG) -t $(IMAGE_REPO):$(LATEST_TAG) .
.PHONY: image.push
image.push:
@if ! command -v git >/dev/null 2>&1; then \
echo "Error: git is not installed. Cannot determine IMAGE_TAG."; exit 1; \
fi; \
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then \
echo "Error: Not a git repository. Cannot determine IMAGE_TAG."; exit 1; \
fi; \
$(DOCKER) push $(IMAGE_REPO):$(IMAGE_TAG)
$(DOCKER) push $(IMAGE_REPO):$(LATEST_TAG)

Copilot uses AI. Check for mistakes.
Comment on lines 174 to 190
- 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
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deploy user is added to the docker group (line 177), but Docker needs to be restarted or the user needs to log out/in for group membership to take effect. The subsequent docker commands (lines 187, 193) run as the deploy user might fail on first deployment if the user's group membership hasn't been refreshed. Consider adding newgrp docker or using sg docker -c for the docker commands.

Copilot uses AI. Check for mistakes.
@@ -1,15 +1,18 @@
idontneedit.org.ru {
{$SITE_DOMAIN} {
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Caddyfile references environment variable {$SITE_DOMAIN}, but this variable is not explicitly set in docker-compose.yml. While it's defined in the env.j2 template and .env.example, the Caddy container needs this variable to be available. Ensure that docker-compose.yml passes this environment variable to the caddy service, either through env_file or explicitly in the environment section.

Copilot uses AI. Check for mistakes.
ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*

RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin app \
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The user is created with shell /usr/sbin/nologin, which is appropriate for security. However, this will prevent interactive debugging if needed. Consider documenting this choice or using /bin/false which is more commonly used for this purpose.

Suggested change
RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin app \
RUN useradd --uid 10001 --create-home --shell /bin/false app \

Copilot uses AI. Check for mistakes.
env = environ.Env(
DJANGO_PRODUCTION=(bool, False),
DJANGO_DEBUG=(bool, False),
DJANGO_SECRET_KEY=(str, "django-insecure-please-change-me-in-production"),
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The default value for DJANGO_SECRET_KEY is "django-insecure-please-change-me-in-production" which includes the word "insecure". Django 6.0rc1 might have checks that warn about or reject secret keys containing "insecure". While this is intended as a placeholder, it could cause confusion. Consider using a different placeholder value or adding validation to ensure this default is never used in production.

Copilot uses AI. Check for mistakes.
restart: unless-stopped

app:
image: ${IMAGE_NAME:-histrio/idontneedit:latest}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The pull_policy: always will always attempt to pull the image even if it exists locally. This is correct for production deployments to ensure the latest image is used, but it will cause slower startups. Document this behavior or consider using pull_policy: missing for development environments via docker-compose.override.yml.

Suggested change
image: ${IMAGE_NAME:-histrio/idontneedit:latest}
image: ${IMAGE_NAME:-histrio/idontneedit:latest}
# pull_policy: always ensures the latest image is always used (recommended for production).
# For development, this may cause slower startups. You can override this in docker-compose.override.yml:
# app:
# pull_policy: missing

Copilot uses AI. Check for mistakes.
DJANGO_STATIC_ROOT=(str, "/app/static/"),
)

PRODUCTION = env.bool("DJANGO_PRODUCTION")
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .env file is only read when DJANGO_PRODUCTION is False. However, in the Dockerfile and docker-compose.yml, environment variables are passed directly and DJANGO_PRODUCTION=true is set in production. This means the .env file reading logic in lines 32-33 will never execute in production (which is correct), but it also means that in development with docker-compose, if DJANGO_PRODUCTION is not explicitly set to false or is missing, the .env file won't be read. Consider explicitly documenting this behavior or ensuring the development docker-compose.override.yml sets DJANGO_PRODUCTION=false if you want to use .env files in containerized development.

Suggested change
PRODUCTION = env.bool("DJANGO_PRODUCTION")
PRODUCTION = env.bool("DJANGO_PRODUCTION")
# The .env file is only read when DJANGO_PRODUCTION is False.
# In containerized development (e.g., Docker Compose), ensure DJANGO_PRODUCTION=false is set
# if you want to use .env files for configuration. Otherwise, environment variables are used.

Copilot uses AI. Check for mistakes.
Comment on lines 186 to 190
- name: pull latest images via Docker Compose
ansible.builtin.command: docker compose pull
args:
chdir: "{{ dest }}"
become_user: deploy
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker compose pull command is executed before bringing up the stack. However, if the app service is configured to build locally (via docker-compose.override.yml), the pull will fail or pull an outdated image. Consider adding a condition to check if local build is being used, or document that this playbook is only for pulling pre-built images from the registry.

Copilot uses AI. Check for mistakes.
@histrio histrio self-assigned this Dec 2, 2025
@histrio histrio merged commit 112b1d2 into main Dec 3, 2025
3 checks passed
@histrio histrio deleted the feat/infra-add-dockerfile-compose-containerized-deploy branch December 3, 2025 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants