I am preparing a release in production for a Dockerized Django-admin blog.
settings.py:
"""
Django settings for XXXX project.
Generated by 'django-admin startproject' using Django 3.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import os
from pathlib import Path
from django.contrib.messages import constants as messages
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
TEMPLATES_DIRS = os.path.join(BASE_DIR,'templates')
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY') or 'XXXXXXX'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
DEBUG = os.getenv('ENVIRONMENT') != 'production'
SESSION_COOKIE_SECURE = True
if DEBUG:
# ALLOWED_HOSTS = ['127.0.0.1', 'XXX.XX.XX.XX', 'localhost']
ALLOWED_HOSTS = ['*']
else:
ALLOWED_HOSTS = ['{ALLOWED_HOST_1}', '{ALLOWED_HOST_2}']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.staticfiles',
'django.contrib.sitemaps',
'widget_tweaks',
'blog',
'ckeditor',
'taggit',
'hitcount'
]
DEFAULT_FROM_EMAIL = 'XXXXXXX'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'admin.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [TEMPLATES_DIRS],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'libraries':{
'is_current': 'blog.templatetags.is_current'
}
}
},
]
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
MESSAGE_TAGS = {
messages.DEBUG: 'alert-secondary',
messages.INFO: 'alert-info',
messages.SUCCESS: 'alert-success',
messages.WARNING: 'alert-warning',
messages.ERROR: 'alert-danger',
}
WSGI_APPLICATION = 'admin.wsgi.application'
LANGUAGE_CODE = 'fr-fr' # or other appropriate code
USE_I18N = True
USE_L10N = True
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': os.getenv('DB_NAME') or 'XXXXXX',
'USER': os.getenv('DB_USER') or 'XXXX',
'PASSWORD': os.getenv('DB_PASSWORD') or 'XXXX',
'HOST': os.getenv('DB_HOST') or 'XXXX'
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
},
'handlers': {
'console': {
'level': 'NOTSET',
'class': 'logging.StreamHandler',
'formatter': 'verbose'
}
},
'loggers': {
'': {
'handlers': ['console'],
'level': 'NOTSET',
},
'django.request': {
'handlers': ['console'],
'propagate': False,
'level': 'ERROR'
}
}
}
HITCOUNT_KEEP_HIT_ACTIVE = { 'seconds': 1 }
MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/')
MEDIA_URL = '/media/'
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'fr-FR'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
if not DEBUG:
# Static files are built with "python manage.py collectstatic" to "dist" directory and then served by nginx
STATIC_ROOT = os.path.join(BASE_DIR, 'dist')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
CSP_DEFAULT_SRC = ["'none'"]
CSP_SCRIPT_SRC = [
"https://stackpath.bootstrapcdn.com",
"https://cdn.jsdelivr.net",
"https://code.jquery.com",
"https://fonts.googleapis.com",
'self'
]
CSP_STYLE_SRC = ["https://fonts.googleapis.com"]
CSP_FONT_SRC = [
"https://fonts.googleapis.com"
]
CSP_IMG_SRC = ["'self'"]
CSP_FRAME_SRC = [
"https://app.mailjet.com",
"https://fonts.googleapis.com"
]
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
# DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Dockerfile for the app:
FROM python:3
ENV PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.txt /code
RUN pip install -r requirements.txt
COPY . /code/
Dockerfile for DB:
FROM postgres:9.6
COPY ./docker/db/init-script.sh /docker-entrypoint-initdb.d/init-script.sh
Init-script.sh for DB:
#!/bin/bash
set -e
if [ ! -z $DB_USER ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
CREATE DATABASE "$DB_NAME";
GRANT ALL PRIVILEGES ON DATABASE "$DB_NAME" TO $DB_USER;
EOSQL
fi
Dockerfile for Nginx:
FROM nginx:1.21.3-alpine
# Configuration
COPY ./docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Install the packages required for watchman to work properly:
RUN apk add --no-cache libcrypto1.0 libgcc libstdc++
# Copy the watchman executable binary directly from our image:
COPY --from=icalialabs/watchman:4-alpine3.4 /usr/local/bin/watchman* /usr/local/bin/
# Create the watchman STATEDIR directory:
RUN mkdir -p /usr/local/var/run/watchman \
&& touch /usr/local/var/run/watchman/.not-empty
# (Optional) Copy the compiled watchman documentation:
COPY --from=icalialabs/watchman:4-alpine3.4 /usr/local/share/doc/watchman* /usr/local/share/doc/
# Continue with the rest of your Dockerfile...
# Static files
COPY ./dist/ /var/www/XXXX/static
Nginx.conf:
server {
listen 80;
server_name XXXXX.fr;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
root /var/www/XXXXX;
}
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://unix:/run/gunicorn.sock;
}
}
Below my docker-compose.yml:
version: "3"
services:
db:
build:
dockerfile: ./docker/db/Dockerfile
context: .
volumes:
- ./data/dev:/var/lib/postgresql/data:z
environment:
- POSTGRES_DB=XXXX
- POSTGRES_USER=XX
- POSTGRES_PASSWORD=XX
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d XXXXX"]
interval: 10s
timeout: 10s
retries: 5
app:
build:
dockerfile: ./docker/app/Dockerfile
context: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
environment:
- DB_HOST=XXX
- DB_NAME=XXX
- DB_USER=XXXX
- DB_PASSWORD=XXXX
depends_on:
db:
condition: service_healthy
Here my docker-compose.prod.yml:
version: "3"
services:
app:
build:
dockerfile: ./docker/app/Dockerfile
context: .
command: gunicorn --access-logformat "{'host':'%({host}i)s','remote_ip':'%(h)s','response_code':'%(s)s','request_method':'%(m)s','request_path':'%(U)s','request_querystring':'%(q)s','request_timetaken':'%(D)s','response_length':'%(B)s'}" --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock admin.wsgi:application
volumes:
- .:/code
- ./docker/app/sock:/run
environment:
- ENVIRONMENT=production
- DB_HOST=XX
- DB_NAME=XX
- DB_USER=XX
- DB_PASSWORD=XXX
- SECRET_KEY=XXXXX
depends_on:
db:
condition: service_healthy
db:
volumes:
- ./data/prod:/var/lib/postgresql/data
environment:
- DB_USER=XXXXX
- DB_PASSWORD=XXXXX
- DB_NAME=XXXXX
nginx:
build:
dockerfile: ./docker/nginx/Dockerfile
context: .
volumes:
- ./docker/app/sock:/run
ports:
- "80:80"
On my host machine when I run docker-compose exec app python manage.py collectstatic
here is the traceback:
Traceback (most recent call last):
File "/code/[manage.py](https://manage.py/)", line 22, in <module> main()
File "/code/[manage.py](https://manage.py/)", line 18, in main execute_from_command_line(sys.argv)
File "/usr/local/lib/python3.11/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line utility.execute()
File "/usr/local/lib/python3.11/site-packages/django/core/management/__init__.py", line 413, in execute self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.11/site-packages/django/core/management/[base.py](https://base.py/)", line 354, in run_from_argv self.execute(*args, **cmd_options) File "/usr/local/lib/python3.11/site-packages/django/core/management/[base.py](https://base.py/)", line 398, in execute output = self.handle(*args, **options) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/contrib/staticfiles/management/commands/[collectstatic.py](https://collectstatic.py/)", line 187, in handle collected = self.collect() ^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/contrib/staticfiles/management/commands/[collectstatic.py](https://collectstatic.py/)", line 114, in collect handler(path, prefixed_path, storage)
File "/usr/local/lib/python3.11/site-packages/django/contrib/staticfiles/management/commands/[collectstatic.py](https://collectstatic.py/)", line 338, in copy_file if not self.delete_file(path, prefixed_path, source_storage): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/contrib/staticfiles/management/commands/[collectstatic.py](https://collectstatic.py/)", line 248, in delete_file if self.storage.exists(prefixed_path): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/core/files/[storage.py](https://storage.py/)", line 318, in exists return os.path.exists(self.path(name)) ^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/contrib/staticfiles/[storage.py](https://storage.py/)", line 38, in path raise ImproperlyConfigured("You're using the staticfiles app " django.core.exceptions.ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path.
This seems to be an issue with the STATIC_ROOT config on the settings.py side, I might be able to find a workaround. However, when I try to use docker-compose.yml with docker-compose.prod.yml, it fails on my host machine while the container is of course running. Please find the traceback:
Usage: docker compose [OPTIONS] COMMAND
Docker Compose
Options:
--ansi string Control when to print ANSI control characters
("never"|"always"|"auto") (default "auto")
--compatibility Run compose in backward compatibility mode
--env-file string Specify an alternate environment file.
-f, --file stringArray Compose configuration files
--parallel int Control max parallelism, -1 for unlimited (default -1)
--profile stringArray Specify a profile to enable
--project-directory string Specify an alternate working directory
(default: the path of the, first specified, Compose file)
-p, --project-name string Project name
Commands:
build Build or rebuild services
convert Converts the compose file to platform's canonical format
cp Copy files/folders between a service container and the local filesystem
create Creates containers for a service.
down Stop and remove containers, networks
events Receive real time events from containers.
exec Execute a command in a running container.
images List images used by the created containers
kill Force stop service containers.
logs View output from containers
ls List running compose projects
pause Pause services
port Print the public port for a port binding.
ps List containers
pull Pull service images
push Push service images
restart Restart service containers
rm Removes stopped service containers
run Run a one-off command on a service.
start Start services
stop Stop services
top Display the running processes
unpause Unpause services
up Create and start containers
version Show the Docker Compose version information
Run 'docker compose COMMAND --help' for more information on a command.
unknown docker command: "compose python" same with docker compose
And the traceback from the running terminal:
app | Traceback (most recent call last):
app | File "/code/manage.py", line 22, in <module>
app | main()
app | File "/code/manage.py", line 18, in main
app | execute_from_command_line(sys.argv)
app | File "/usr/local/lib/python3.11/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
app | utility.execute()
app | File "/usr/local/lib/python3.11/site-packages/django/core/management/__init__.py", line 363, in execute
app | settings.INSTALLED_APPS
app | File "/usr/local/lib/python3.11/site-packages/django/conf/__init__.py", line 82, in getattr
app | self._setup(name)
app | File "/usr/local/lib/python3.11/site-packages/django/conf/__init__.py", line 69, in _setup
app | self._wrapped = Settings(settings_module)
app | ^^^^^^^^^^^^^^^^^^^^^^^^^
app | File "/usr/local/lib/python3.11/site-packages/django/conf/__init__.py", line 170, in init
app | mod = importlib.import_module(self.SETTINGS_MODULE)
app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app | File "/usr/local/lib/python3.11/importlib/__init__.py", line 126, in import_module
app | return _bootstrap._gcd_import(name[level:], package, level)
app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app | File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
app | File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
app | File "<frozen importlib._bootstrap>", line 1149, in _find_and_load_unlocked
app | File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
app | File "<frozen importlib._bootstrap_external>", line 936, in exec_module
app | File "<frozen importlib._bootstrap_external>", line 1074, in get_code
app | File "<frozen importlib._bootstrap_external>", line 1004, in source_to_code
app | File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
app | File "/code/admin/settings.py", line 32
app | DEBUG = os.getenv('ENVIRONMENT') =! 'production'
app | ^^^^^^^^^^^^^^^^^^^^^^^^
app | SyntaxError: cannot assign to function call
On the droplet/Ubuntu server, the command docker-compose -f docker-compose.yml -f docker-compose.prod.yml python manage.py collectstatic
gives me nothing, just an empty output and the dist directory is not created.
Sorry for the long message.
My host machine is on Arch Linux (6.1.8-arch1-1) and the server on Ubuntu 20.04 (LTS) x64.
Thanks a lot!