Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git
__pycache__
*.pyc
venv
.env
db.sqlite3
static
media
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
FROM python:3.12-slim
LABEL maintainer="your_email@example.com"

ENV PYTHONUNBUFFERED 1
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
bash \
gosu \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Comment on lines +7 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You install libpq-dev in this RUN and never remove it. That leaves build-time libraries in the final image and violates the requirement to keep images thin. Install build dependencies (libpq-dev, gcc, libc6-dev, etc.) and run pip install -r requirements.txt, then purge the build deps in the same RUN (or use a multi-stage build / psycopg2-binary) so they are not present in the final image layer.


COPY requirements.txt .

RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libc6-dev \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get purge -y --auto-remove gcc libc6-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Comment thread
Garichka marked this conversation as resolved.
Comment on lines +16 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This RUN correctly installs gcc/libc6-dev, runs pip install and then purges those packages in the same layer — good. However because libpq-dev was installed in a previous layer (see lines 7-12) the image still contains build deps. Combine libpq-dev with these build deps and the pip install in one RUN (and purge afterwards) to keep the image thin.


RUN adduser --disabled-password --no-create-home --shell /bin/bash django-user && \
mkdir -p /vol/web/media /vol/web/static && \
chown -R django-user:django-user /vol && \
chmod -R 755 /vol
Comment on lines +24 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You create /vol/web/media and /vol/web/static and chown them at build time. Named volumes mounted at runtime are created as root and can override ownership. Make sure you also chown these paths at container start (in the entrypoint, before dropping privileges) so collectstatic, migrations and file uploads won't fail when the container runs as a non-root user.


COPY . .
RUN chown -R django-user:django-user /app

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

ENTRYPOINT ["/entrypoint.sh"]
Comment on lines +32 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You copy the entrypoint and set it as ENTRYPOINT here. Verify the entrypoint script executes the whole command chain under the intended user. Avoid a pattern like exec gosu user python manage.py wait_for_db && python manage.py migrate ... because exec replaces the shell and the && sequence following it will not be executed. Instead run the full chain under the dropped-privilege context, for example: exec gosu django-user sh -c "python manage.py wait_for_db && python manage.py migrate && python manage.py collectstatic --no-input && python manage.py runserver 0.0.0.0:8000" (or run migrations/collectstatic as root, chown volumes, then exec the server as the non-root user).

Empty file added cinema/management/__init__.py
Empty file.
Empty file.
29 changes: 29 additions & 0 deletions cinema/management/commands/wait_for_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import time
Comment thread
Garichka marked this conversation as resolved.
from django.db import connections
from django.db.utils import OperationalError
from django.core.management.base import BaseCommand, CommandError


class Command(BaseCommand):
def handle(self, *args, **options):
self.stdout.write("Waiting for database...")
attempts = 0
max_attempts = 20

Comment thread
Garichka marked this conversation as resolved.
Comment on lines +7 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You install libpq-dev in this RUN but never remove it later. To keep the image thin move runtime/build installs into a single RUN where you install build deps, run pip install -r requirements.txt, then purge the build packages (or use psycopg2-binary / multi-stage build). Leaving libpq-dev installed contradicts the 'thin image' requirement.

while attempts < max_attempts:
Comment thread
Garichka marked this conversation as resolved.
try:
db_conn = connections["default"]
db_conn.cursor()
self.stdout.write(self.style.SUCCESS(
f"Database available after {attempts + 1} attempt(s)!"
Comment thread
Garichka marked this conversation as resolved.
))
return
except OperationalError:
Comment thread
Garichka marked this conversation as resolved.
Comment thread
Garichka marked this conversation as resolved.
attempts += 1
Comment on lines +16 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This RUN installs gcc and libc6-dev, runs pip install and then purges those packages in the same layer — this approach is good. Keep the install/pip/purge in the same RUN so build deps are not left in the final image (as you did here).

self.stdout.write(
f"Database unavailable "
f"(attempt {attempts}/{max_attempts}), waiting 1s..."
)
time.sleep(1)
Comment on lines +24 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Chowning /vol during build only affects files baked into the image; it does not change ownership of named volumes mounted at runtime (they are created as root). Ensure you perform a runtime chown on /vol/web/static and /vol/web/media in the entrypoint (run as root) before dropping privileges so collectstatic/uploads won't fail.


raise CommandError("Database unavailable after maximum attempts.")
29 changes: 18 additions & 11 deletions cinema_service/settings.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
"""
Django settings for cinema_service project.

Generated by 'django-admin startproject' using Django 4.0.4.
Generated by "django-admin startproject" using Django 4.0.4.

For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
Comment thread
Garichka marked this conversation as resolved.

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path

Comment thread
Garichka marked this conversation as resolved.
# Build paths inside the project like this: BASE_DIR / 'subdir'.
# Build paths inside the project like this: BASE_DIR / "subdir".
BASE_DIR = Path(__file__).resolve().parent.parent


Comment thread
Garichka marked this conversation as resolved.
Expand All @@ -24,10 +25,10 @@
"django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3"
)

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
# SECURITY WARNING: don"t run with debug turned on in production!
DEBUG = os.getenv("DEBUG", "False") == "True"

ALLOWED_HOSTS = []
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")

INTERNAL_IPS = [
"127.0.0.1",
Expand Down Expand Up @@ -86,12 +87,22 @@

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME", "cinema"),
"USER": os.getenv("DB_USER", "postgres"),
"PASSWORD": os.getenv("DB_PASS", "postgres"),
"HOST": os.getenv("DB_HOST", "db"),
"PORT": os.getenv("DB_PORT", "5432"),
}
}


STATIC_URL = "/static/"
MEDIA_URL = "/media/"

STATIC_ROOT = "/vol/web/static"
MEDIA_ROOT = "/vol/web/media"

# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

Expand Down Expand Up @@ -131,10 +142,6 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/

STATIC_URL = "static/"

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
Expand Down
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: "3.8"

services:
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=cinema
Comment on lines +6 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This line sequence uses exec gosu django-user python manage.py wait_for_db && .... exec will replace the shell with the first command so the following && chain won't be executed; as a result migrations/collectstatic/runserver will not run and the container may exit. Run the whole chain under the non-root user, e.g. exec gosu django-user sh -c "python manage.py wait_for_db && python manage.py migrate && ...", or run chown/migrations/collectstatic as root then exec gosu django-user ... for the final server.

Comment on lines +6 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The entrypoint doesn't forward/exec provided arguments, so commands like docker-compose run app sh -c "python manage.py test" or docker-compose run app sh -c "flake8" will not work. Make the entrypoint support argument forwarding (for example, if [ "$#" -gt 0 ]; then exec "$@"; fi) or use the common pattern exec gosu django-user "$@" after runtime setup.

- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres

app:
build: .
ports:
- "8000:8000"
volumes:
- media_data:/vol/web/media
- static_data:/vol/web/static
Comment thread
Garichka marked this conversation as resolved.
Comment on lines +17 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Named volumes for media/static are created here, but Docker creates new volumes as root by default. Ensure you change ownership at container startup (in the entrypoint) before dropping privileges so collectstatic, migrations and file uploads won't fail due to permission issues.

environment:
- DB_HOST=db
- DB_NAME=cinema
- DB_USER=postgres
- DB_PASS=postgres
- ALLOWED_HOSTS=127.0.0.1,localhost
- DEBUG=False
depends_on:
- db
Comment thread
Garichka marked this conversation as resolved.
Comment thread
Garichka marked this conversation as resolved.

volumes:
postgres_data:
media_data:
static_data:
Comment thread
Garichka marked this conversation as resolved.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ djangorestframework
djangorestframework-simplejwt
drf-spectacular
Pillow
psycopg2-binary>=2.9.9
Empty file added scripts/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
set -e

chown -R django-user:django-user /vol/web
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This chown in the entrypoint is good (it runs at container start), but be explicit about the exact paths you need (/vol/web/static and /vol/web/media) and ensure it runs before any collectstatic/migrate commands and before dropping privileges.


exec gosu django-user python manage.py wait_for_db && \
python manage.py migrate && \
python manage.py collectstatic --no-input && \
python manage.py runserver 0.0.0.0:8000
Comment on lines +6 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Using exec gosu django-user python manage.py wait_for_db && \ ... is problematic: exec replaces the shell with the first command, so the chained commands after && will not be executed. Instead either run the whole chain under a single sh -c executed via gosu (e.g. exec gosu django-user sh -c "python manage.py wait_for_db && python manage.py migrate && ...") or run wait_for_db/migrate/collectstatic as root (after chown) and only exec the final runserver as the non-root user. Also ensure the script forwards arguments so docker-compose run app sh -c "python manage.py test" works.

Loading