Skip to content

Commit 18f35c5

Browse files
authored
Docker Image Monolith (#922)
2 parents 1d1bc7e + eedc5c2 commit 18f35c5

37 files changed

+573
-535
lines changed

.dockerignore

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
**/__pycache__/
2+
**/.pytest_cache/
3+
**/htmlcov/
4+
**/.github/
5+
**/.env/
6+
**/.venv/
7+
**/.vscode/
8+
**/.git/
9+
**/dist/
10+
**/node_modules/
11+
12+
**/Dockerfile
13+
**/compose.yml
14+
**/.coverage
15+
**/.dockerignore
16+
**/Taskfile.yml

.env.docker

-3
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,3 @@ TEKST_ES__HOST=es
228228

229229
# TEKST_MISC__DEL_EXPORTS_AFTER_MINUTES=5
230230
# default: 5
231-
232-
# TEKST_MISC__DEMO_DATA_PATH=Tekst-API/demo
233-
# default: Tekst-API/demo (default is set programmatically)
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Create and publish Docker image
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
8+
env:
9+
REGISTRY: ghcr.io
10+
IMAGE_NAME: ${{ github.repository }}
11+
12+
jobs:
13+
build-and-push-image:
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
packages: write
18+
attestations: write
19+
id-token: write
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
- name: Log in to the Container registry
24+
uses: docker/login-action@v3
25+
with:
26+
registry: ${{ env.REGISTRY }}
27+
username: ${{ github.repository_owner }}
28+
password: ${{ secrets.GITHUB_TOKEN }}
29+
- name: Extract metadata (tags, labels) for Docker
30+
id: meta
31+
uses: docker/metadata-action@v5
32+
with:
33+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
34+
- name: Set up Docker Buildx
35+
uses: docker/setup-buildx-action@v3
36+
- name: Build and push Docker image
37+
id: push
38+
uses: docker/build-push-action@v6
39+
with:
40+
context: .
41+
push: true
42+
tags: ${{ steps.meta.outputs.tags }}
43+
labels: ${{ steps.meta.outputs.labels }}
44+
- name: Generate artifact attestation
45+
uses: actions/attest-build-provenance@v2
46+
with:
47+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
48+
subject-digest: ${{ steps.push.outputs.digest }}
49+
push-to-registry: true

Dockerfile

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
2+
# TEKST-WEB BUILDER IMAGE
3+
4+
FROM node:22.15.0-alpine3.20 AS web-builder
5+
WORKDIR /tekst
6+
COPY Tekst-Web/ .
7+
RUN npm install && npm run build-only -- --base=./
8+
9+
10+
# PYTHON ALPINE BASE IMAGE
11+
12+
FROM python:3.13-alpine3.21 AS py-base
13+
ENV PYTHONFAULTHANDLER=1 \
14+
PYTHONHASHSEED=random \
15+
PYTHONUNBUFFERED=1 \
16+
PYTHONDONTWRITEBYTECODE=1 \
17+
PIP_DEFAULT_TIMEOUT=100 \
18+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
19+
PIP_NO_CACHE_DIR=1 \
20+
PIP_ROOT_USER_ACTION=ignore
21+
22+
23+
# TEKST-API BUILDER IMAGE
24+
25+
FROM py-base AS api-builder
26+
WORKDIR "/tekst"
27+
28+
COPY --from=ghcr.io/astral-sh/uv:0.6.16 /uv /uvx /bin/
29+
COPY Tekst-API/tekst/ ./tekst/
30+
COPY Tekst-API/uv.lock* \
31+
Tekst-API/pyproject.toml \
32+
Tekst-API/README.md \
33+
Tekst-API/LICENSE \
34+
./
35+
36+
RUN uv run pip install --upgrade \
37+
pip \
38+
setuptools \
39+
wheel
40+
41+
RUN uv export \
42+
--locked \
43+
--no-group dev \
44+
--no-hashes \
45+
--no-progress \
46+
--format requirements-txt \
47+
--output-file requirements.txt
48+
49+
RUN uv run pip wheel \
50+
--requirement requirements.txt \
51+
--wheel-dir deps
52+
53+
RUN uv build --wheel
54+
55+
56+
# FINAL PRODUCTION IMAGE
57+
58+
FROM py-base AS prod
59+
ENV FASTAPI_ENV=production
60+
WORKDIR "/tekst"
61+
62+
RUN set -x && \
63+
addgroup -S tekst && \
64+
adduser -S tekst -G tekst
65+
66+
RUN apk update && \
67+
apk add --no-cache curl caddy
68+
69+
HEALTHCHECK \
70+
--interval=2m \
71+
--timeout=5s \
72+
--retries=3 \
73+
--start-period=30s \
74+
CMD curl http://localhost:8000/status || exit 1
75+
76+
COPY --from=api-builder /tekst/deps/ api/deps/
77+
COPY --from=api-builder /tekst/dist/ api/dist/
78+
COPY --from=web-builder /tekst/dist/ /var/www/html/
79+
80+
RUN chown -R tekst:tekst /var/www/html/
81+
82+
RUN python3 -m pip install \
83+
--no-index \
84+
--find-links api/deps/ \
85+
api/dist/*.whl && \
86+
rm -rf api
87+
88+
RUN python3 -m pip install \
89+
"uvicorn[standard]==0.32.0" \
90+
"gunicorn==23.0.0"
91+
92+
COPY docker/caddy/Caddyfile /etc/caddy/Caddyfile
93+
COPY docker/gunicorn/gunicorn_conf.py /etc/gunicorn/
94+
COPY docker/entrypoint.sh /usr/local/bin/
95+
96+
VOLUME /var/www/tekst/static/
97+
EXPOSE 80
98+
USER tekst
99+
100+
ENTRYPOINT ["entrypoint.sh"]
101+
CMD ["gunicorn", "tekst.app:app", "--config", "/etc/gunicorn/gunicorn_conf.py"]

Tekst-API/.env.template

-3
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,3 @@
228228

229229
# TEKST_MISC__DEL_EXPORTS_AFTER_MINUTES=5
230230
# default: 5
231-
232-
# TEKST_MISC__DEMO_DATA_PATH=Tekst-API/demo
233-
# default: Tekst-API/demo (default is set programmatically)

Tekst-API/.env.test

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# basics
2-
TEKST_API_PATH=
32
TEKST_DEV_MODE=true
43
TEKST_LOG_LEVEL=debug
54
TEKST_AUTO_MIGRATE=true

Tekst-API/Dockerfile

-112
This file was deleted.

Tekst-API/entrypoint.sh

-14
This file was deleted.

Tekst-API/pyproject.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ dependencies = [
2222
"bleach<7.0.0,>=6.1.0",
2323
"jsonref<2.0.0,>=1.1.0",
2424
"elasticsearch<9.0.0,>=8.17.1",
25-
"fastapi[standard]>=0.115.6",
25+
"fastapi>=0.115.6",
2626
]
2727
keywords = [
2828
"text",
@@ -67,6 +67,8 @@ dev = [
6767
"asgi-lifespan<3.0.0,>=2.1.0",
6868
"ruff<1.0.0,>=0.8.0",
6969
"pytest-env<2.0.0,>=1.1.1",
70+
"uvicorn[standard]>=0.34.2",
71+
"fastapi-cli>=0.0.7",
7072
]
7173

7274
# ruff

Tekst-API/tekst/auth.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -427,27 +427,27 @@ async def _create_user(user: UserCreate) -> UserRead:
427427
return await user_manager.create(user, safe=False)
428428

429429

430-
async def create_initial_superuser(force: bool = False):
431-
if _cfg.dev_mode and not force:
430+
async def create_initial_superuser(cfg: TekstConfig = _cfg):
431+
if cfg.dev_mode:
432432
return
433433
log.info("Creating initial superuser account...")
434434
# check if user collection contains users, abort if so
435435
if await UserDocument.find_one().exists(): # pragma: no cover
436436
log.warning(
437437
"User collection already contains users. "
438-
f"Skipping creation of inital admin {_cfg.security.init_admin_email}."
438+
f"Skipping creation of inital admin {cfg.security.init_admin_email}."
439439
)
440440
return
441441
# check if initial admin account is properly configured
442442
if (
443-
not _cfg.security.init_admin_email or not _cfg.security.init_admin_password
443+
not cfg.security.init_admin_email or not cfg.security.init_admin_password
444444
): # pragma: no cover
445445
log.warning("No initial admin account configured, skipping creation.")
446446
return
447447
# create inital admin account
448448
user = UserCreate(
449-
email=_cfg.security.init_admin_email,
450-
password=_cfg.security.init_admin_password,
449+
email=cfg.security.init_admin_email,
450+
password=cfg.security.init_admin_password,
451451
username="admin",
452452
name="Admin Admin",
453453
affiliation="Admin",
@@ -457,6 +457,6 @@ async def create_initial_superuser(force: bool = False):
457457
user.is_superuser = True
458458
await _create_user(user)
459459
log.warning(
460-
f"Created initial admin account for email {_cfg.security.init_admin_email}. "
461-
"PLEASE CHANGE THIS ACCOUNT'S EMAIL AND PASSWORD IMMEDIATELY!"
460+
f"Created initial admin account for email {cfg.security.init_admin_email}. "
461+
"PLEASE CHANGE ITS PASSWORD ASAP!"
462462
)

Tekst-API/tekst/config.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,11 @@ class MiscConfig(ConfigSubSection):
285285
usrmsg_force_delete_after_days: int = 365
286286
max_resources_per_user: int = 10
287287
del_exports_after_minutes: int = 5
288-
demo_data_path: DirectoryPath = Path(realpath(__file__)).parent.parent / "demo"
288+
289+
@computed_field
290+
@property
291+
def demo_data_path(self) -> str:
292+
return Path(realpath(__file__)).parent.parent / "demo"
289293

290294

291295
class TekstConfig(BaseSettings):

0 commit comments

Comments
 (0)