Docker stuff
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 3 Nov 2026 12:04:53 +0000 (13:04 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 3 Nov 2026 12:04:53 +0000 (13:04 +0100)
Dockerfile
docker-compose.yml
scripts/fold_hunspell.py [new file with mode: 0644]
scripts/nginx.conf [new file with mode: 0644]
scripts/postgres-polish.sql [new file with mode: 0644]

index b23530b..2fc76de 100644 (file)
@@ -1,7 +1,9 @@
+# Base system and requirements.
+
 FROM python:3.9-trixie AS base
 
-ARG UID=1000
-ARG GID=1000
+ARG UID=8100
+ARG GID=8100
 
 RUN     apt-get update && apt-get install -y \
        git \
@@ -39,15 +41,18 @@ RUN mkdir /app/.ipython
 
 
 
+# dev
+# Assumes code will be mounted.
 
 FROM base AS dev
 
 #RUN pip install --no-cache-dir coverage
 
 
+# prod-base
+# Installs code and production requirements.
 
-
-FROM base AS prod
+FROM base AS prod-base
 
 USER root
 
@@ -58,16 +63,66 @@ USER app
 COPY src /app/src
 
 
+# static-builder
+# Static files are collected here. Statics are then copied into an Nginx image,
+# and staticfiles.json manifest file is copied back into production image.
 
-
-FROM prod AS static-builder
+FROM prod-base AS static-builder
 
 RUN python manage.py collectstatic --noinput
 
 
+# nginx
+# Contains static files and nginx config.
 
 FROM nginx:alpine AS nginx
 
+ARG UID=8100
+ARG GID=8100
+
 # COPY nginx.conf conf.d/...
 
+RUN addgroup -g $GID app && addgroup nginx app
 COPY --from=static-builder /app/var/static /app/var/static
+
+
+
+
+# Postgres with Polish hunspell dictionary withoud diacritics
+# to allow full text search without diacritics.
+
+# --- Stage 1: prepare dictionary files ---
+FROM python:3.13-slim-trixie AS polish-dict
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends hunspell-pl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY scripts/fold_hunspell.py /usr/local/bin/fold_hunspell.py
+
+RUN set -eu; \
+    mkdir /out; cd /tmp; \
+    enc="$(sed -n 's/^SET[[:space:]]*//p' /usr/share/hunspell/pl_PL.aff | tr -d '\r')"; \
+    iconv -f "${enc:-UTF-8}" -t UTF-8 /usr/share/hunspell/pl_PL.dic > pl.dict; \
+    iconv -f "${enc:-UTF-8}" -t UTF-8 /usr/share/hunspell/pl_PL.aff > pl.affix; \
+    python3 /usr/local/bin/fold_hunspell.py pl.dict pl.affix \
+        /out/polish.dict /out/polish.affix; \
+    dpkg-query -W -f='hunspell-pl ${Version}\n' hunspell-pl > /out/polish.version
+
+# --- Stage 2: actual Postgres image with the dictionary ---
+FROM postgres:18 AS postgres
+
+COPY --from=polish-dict /out/ /tmp/polish-dict/
+RUN mv /tmp/polish-dict/* "/usr/share/postgresql/$PG_MAJOR/tsearch_data/" \
+ && rmdir /tmp/polish-dict
+
+COPY scripts/postgres-polish.sql /docker-entrypoint-initdb.d/10-postgres-polish.sql
+
+
+
+# prod
+# That's the production image with staticfiles.json manifest file added.
+
+FROM prod-base AS prod
+
+COPY --from=static-builder /app/var/static/staticfiles.json /app/var/static/staticfiles.json
index bda15bb..b01e970 100644 (file)
@@ -26,8 +26,6 @@ x-app-base: &app-base
 services:
   web:
     <<: *app-base
-    ports:
-     - "8000:8000"
     command: ./manage.py runserver 0.0.0.0:8000
 
   celery:
@@ -35,7 +33,9 @@ services:
     command: celery -A wolnelektury worker -l info
 
   db:
-    image: postgres:18
+    build:
+      context: .
+      target: postgres
     env_file:
       - .env
     volumes:
@@ -47,6 +47,21 @@ services:
   memcached:
     image: memcached:1.6-alpine
 
+  nginx:
+    build:
+      context: .
+      target: nginx
+      args:
+        UID: ${UID:-1000}
+        GID: ${GID:-1000}
+    ports:
+      - "127.0.0.1:8101:80"
+    volumes:
+      - ./var/media:/app/var/media:ro
+      - ./scripts/nginx.conf:/etc/nginx/conf.d/default.conf:ro
+    depends_on:
+      - web
+
 
 volumes:
-  ipython:
\ No newline at end of file
+  ipython:
diff --git a/scripts/fold_hunspell.py b/scripts/fold_hunspell.py
new file mode 100644 (file)
index 0000000..bc63e78
--- /dev/null
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+"""
+Tworzy wersję słownika hunspell bez polskich znaków dla PostgreSQL (szablon ispell).
+
+Użycie:
+    fold_hunspell.py polish.dict polish.affix polish_ascii.dict polish_ascii.affix
+
+Pliki wejściowe muszą być w UTF-8 (po iconv z ISO-8859-2).
+
+Samo usunięcie znaków psuje warunki reguł z klasami negowanymi: np.
+`SFX H ć ło [^ąeśź]ć` (wszystko poza ą, e, ś, ź przed ć) po zamianie
+stałoby się `[^aesz]c` i przestało pasować do czasowników na -ać.
+Dlatego litera zostaje w klasie negowanej tylko wtedy, gdy oryginał
+wykluczał wszystkie litery, które się na nią składają; w przeciwnym
+razie jest usuwana, a pusta klasa zamienia się w `.`. Reguły stają się
+przez to nieco zbyt liberalne (nadmiarowe formy), ale żadna forma nie ginie.
+"""
+import re
+import sys
+import unicodedata
+
+
+def fold(text):
+    text = text.replace('ł', 'l').replace('Ł', 'L')
+    return ''.join(
+        c for c in unicodedata.normalize('NFKD', text)
+        if not unicodedata.combining(c)
+    )
+
+
+LETTERS = 'aąbcćdeęfghijklłmnńoóprsśtuvwxyzźż'
+GROUPS = {}
+for letter in LETTERS:
+    GROUPS.setdefault(fold(letter), set()).add(letter)
+
+
+def fold_negated_class(match):
+    excluded = set(match.group(1))
+    keep = []
+    for c in match.group(1):
+        folded = fold(c)
+        if GROUPS.get(folded, {c}) <= excluded and folded not in keep:
+            keep.append(folded)
+    return '[^' + ''.join(keep) + ']' if keep else '.'
+
+
+def fold_affix_line(line):
+    parts = line.split()
+    if len(parts) >= 5 and parts[0] in ('SFX', 'PFX') and '[^' in parts[4]:
+        condition = re.sub(r'\[\^([^\]]*)\]', fold_negated_class, parts[4])
+        line = line.replace(parts[4], condition, 1)
+    if line.startswith('SET '):
+        return 'SET UTF-8\n'
+    return fold(line)
+
+
+def main(dict_in, affix_in, dict_out, affix_out):
+    with open(dict_in, encoding='utf-8') as src, open(dict_out, 'w', encoding='utf-8') as dst:
+        dst.write(fold(src.read()))
+    with open(affix_in, encoding='utf-8') as src, open(affix_out, 'w', encoding='utf-8') as dst:
+        for line in src:
+            dst.write(fold_affix_line(line))
+
+
+if __name__ == '__main__':
+    main(*sys.argv[1:5])
diff --git a/scripts/nginx.conf b/scripts/nginx.conf
new file mode 100644 (file)
index 0000000..872f1a2
--- /dev/null
@@ -0,0 +1,60 @@
+server {
+    listen 80;
+
+    access_log off;
+    error_log /dev/stderr warn;
+
+    client_max_body_size 4G;
+    send_timeout 300s;
+    proxy_buffers 16 16k;
+    proxy_buffer_size 32k;
+
+    location /static/ {
+        root /app/var;
+        expires 30d;
+    }
+
+    location /media/ {
+        root /app/var;
+        expires 30d;
+    }
+    location ~ ^/media/.*\.(mp3|ogg|daisy\.zip)$ {
+        add_header Content-Disposition "attachment";
+    }
+    location ~ ^/media/.*\.txt$ {
+        add_header Content-Type "text/plain; charset=utf-8";
+    }
+    location ~ ^/media/book/(pdf|epub|mobi|fb2|cover|xml)/ {
+        # kill cache
+        add_header Last-Modified $date_gmt;
+        add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
+        if_modified_since off;
+        expires off;
+        etag off;
+    }
+
+    location /api/ {
+        add_header 'Access-Control-Allow-Origin' '*' always;
+        add_header 'Access-Control-Allow-Methods' 'GE, POST, PUT, DELETE' always;
+        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
+        add_header 'Access-Control-Expose-Headers' 'Link' always;
+
+        if ($request_method = OPTIONS) {
+            return 204;
+        }
+
+        proxy_set_header Accept-Language "";
+        proxy_set_header Host $host;
+        proxy_pass http://web:8000;
+    }
+    location / {
+       root /app/var/static/root;
+       try_files $uri @backend;
+    }
+
+    location @backend {
+        proxy_set_header Accept-Language "";
+        proxy_set_header Host $host;
+        proxy_pass http://web:8000;
+    }
+}
diff --git a/scripts/postgres-polish.sql b/scripts/postgres-polish.sql
new file mode 100644 (file)
index 0000000..100764a
--- /dev/null
@@ -0,0 +1,27 @@
+CREATE EXTENSION IF NOT EXISTS unaccent;
+
+DO $$
+  BEGIN
+  IF NOT EXISTS (SELECT 1 FROM pg_ts_dict
+               WHERE dictname = 'polish_hunspell'
+                 AND dictnamespace = 'public'::regnamespace) THEN
+    CREATE TEXT SEARCH DICTIONARY public.polish_hunspell (
+        TEMPLATE = ispell, DictFile = polish, AffFile = polish
+    );
+  END IF;
+  IF NOT EXISTS (SELECT 1 FROM pg_ts_config
+                      WHERE cfgname = 'polish'
+               AND cfgnamespace = 'public'::regnamespace) THEN
+    CREATE TEXT SEARCH CONFIGURATION public.polish (COPY = pg_catalog.simple);
+  END IF;
+END $$;
+
+ALTER TEXT SEARCH DICTIONARY public.polish_hunspell (
+  DictFile = polish, AffFile = polish, StopWords
+);
+
+ALTER TEXT SEARCH CONFIGURATION public.polish
+  ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, word, hword, hword_part
+  WITH unaccent, polish_hunspell, simple;
+
+