Skip to content

Docker Image Monolith #922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 2, 2025
Merged
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
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
**/__pycache__/
**/.pytest_cache/
**/htmlcov/
**/.github/
**/.env/
**/.venv/
**/.vscode/
**/.git/
**/dist/
**/node_modules/

**/Dockerfile
**/compose.yml
**/.coverage
**/.dockerignore
**/Taskfile.yml
3 changes: 0 additions & 3 deletions .env.docker
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,3 @@ TEKST_ES__HOST=es

# TEKST_MISC__DEL_EXPORTS_AFTER_MINUTES=5
# default: 5

# TEKST_MISC__DEMO_DATA_PATH=Tekst-API/demo
# default: Tekst-API/demo (default is set programmatically)
49 changes: 49 additions & 0 deletions .github/workflows/publish-docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Create and publish Docker image

on:
push:
tags:
- "v*.*.*"

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
101 changes: 101 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@

# TEKST-WEB BUILDER IMAGE

FROM node:22.15.0-alpine3.20 AS web-builder
WORKDIR /tekst
COPY Tekst-Web/ .
RUN npm install && npm run build-only -- --base=./


# PYTHON ALPINE BASE IMAGE

FROM python:3.13-alpine3.21 AS py-base
ENV PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_DEFAULT_TIMEOUT=100 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PIP_ROOT_USER_ACTION=ignore


# TEKST-API BUILDER IMAGE

FROM py-base AS api-builder
WORKDIR "/tekst"

COPY --from=ghcr.io/astral-sh/uv:0.6.16 /uv /uvx /bin/
COPY Tekst-API/tekst/ ./tekst/
COPY Tekst-API/uv.lock* \
Tekst-API/pyproject.toml \
Tekst-API/README.md \
Tekst-API/LICENSE \
./

RUN uv run pip install --upgrade \
pip \
setuptools \
wheel

RUN uv export \
--locked \
--no-group dev \
--no-hashes \
--no-progress \
--format requirements-txt \
--output-file requirements.txt

RUN uv run pip wheel \
--requirement requirements.txt \
--wheel-dir deps

RUN uv build --wheel


# FINAL PRODUCTION IMAGE

FROM py-base AS prod
ENV FASTAPI_ENV=production
WORKDIR "/tekst"

RUN set -x && \
addgroup -S tekst && \
adduser -S tekst -G tekst

RUN apk update && \
apk add --no-cache curl caddy

HEALTHCHECK \
--interval=2m \
--timeout=5s \
--retries=3 \
--start-period=30s \
CMD curl http://localhost:8000/status || exit 1

COPY --from=api-builder /tekst/deps/ api/deps/
COPY --from=api-builder /tekst/dist/ api/dist/
COPY --from=web-builder /tekst/dist/ /var/www/html/

RUN chown -R tekst:tekst /var/www/html/

RUN python3 -m pip install \
--no-index \
--find-links api/deps/ \
api/dist/*.whl && \
rm -rf api

RUN python3 -m pip install \
"uvicorn[standard]==0.32.0" \
"gunicorn==23.0.0"

COPY docker/caddy/Caddyfile /etc/caddy/Caddyfile
COPY docker/gunicorn/gunicorn_conf.py /etc/gunicorn/
COPY docker/entrypoint.sh /usr/local/bin/

VOLUME /var/www/tekst/static/
EXPOSE 80
USER tekst

ENTRYPOINT ["entrypoint.sh"]
CMD ["gunicorn", "tekst.app:app", "--config", "/etc/gunicorn/gunicorn_conf.py"]
3 changes: 0 additions & 3 deletions Tekst-API/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,3 @@

# TEKST_MISC__DEL_EXPORTS_AFTER_MINUTES=5
# default: 5

# TEKST_MISC__DEMO_DATA_PATH=Tekst-API/demo
# default: Tekst-API/demo (default is set programmatically)
1 change: 0 additions & 1 deletion Tekst-API/.env.test
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# basics
TEKST_API_PATH=
TEKST_DEV_MODE=true
TEKST_LOG_LEVEL=debug
TEKST_AUTO_MIGRATE=true
Expand Down
112 changes: 0 additions & 112 deletions Tekst-API/Dockerfile

This file was deleted.

14 changes: 0 additions & 14 deletions Tekst-API/entrypoint.sh

This file was deleted.

4 changes: 3 additions & 1 deletion Tekst-API/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"bleach<7.0.0,>=6.1.0",
"jsonref<2.0.0,>=1.1.0",
"elasticsearch<9.0.0,>=8.17.1",
"fastapi[standard]>=0.115.6",
"fastapi>=0.115.6",
]
keywords = [
"text",
Expand Down Expand Up @@ -67,6 +67,8 @@ dev = [
"asgi-lifespan<3.0.0,>=2.1.0",
"ruff<1.0.0,>=0.8.0",
"pytest-env<2.0.0,>=1.1.1",
"uvicorn[standard]>=0.34.2",
"fastapi-cli>=0.0.7",
]

# ruff
Expand Down
16 changes: 8 additions & 8 deletions Tekst-API/tekst/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,27 +427,27 @@ async def _create_user(user: UserCreate) -> UserRead:
return await user_manager.create(user, safe=False)


async def create_initial_superuser(force: bool = False):
if _cfg.dev_mode and not force:
async def create_initial_superuser(cfg: TekstConfig = _cfg):
if cfg.dev_mode:
return
log.info("Creating initial superuser account...")
# check if user collection contains users, abort if so
if await UserDocument.find_one().exists(): # pragma: no cover
log.warning(
"User collection already contains users. "
f"Skipping creation of inital admin {_cfg.security.init_admin_email}."
f"Skipping creation of inital admin {cfg.security.init_admin_email}."
)
return
# check if initial admin account is properly configured
if (
not _cfg.security.init_admin_email or not _cfg.security.init_admin_password
not cfg.security.init_admin_email or not cfg.security.init_admin_password
): # pragma: no cover
log.warning("No initial admin account configured, skipping creation.")
return
# create inital admin account
user = UserCreate(
email=_cfg.security.init_admin_email,
password=_cfg.security.init_admin_password,
email=cfg.security.init_admin_email,
password=cfg.security.init_admin_password,
username="admin",
name="Admin Admin",
affiliation="Admin",
Expand All @@ -457,6 +457,6 @@ async def create_initial_superuser(force: bool = False):
user.is_superuser = True
await _create_user(user)
log.warning(
f"Created initial admin account for email {_cfg.security.init_admin_email}. "
"PLEASE CHANGE THIS ACCOUNT'S EMAIL AND PASSWORD IMMEDIATELY!"
f"Created initial admin account for email {cfg.security.init_admin_email}. "
"PLEASE CHANGE ITS PASSWORD ASAP!"
)
6 changes: 5 additions & 1 deletion Tekst-API/tekst/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,11 @@ class MiscConfig(ConfigSubSection):
usrmsg_force_delete_after_days: int = 365
max_resources_per_user: int = 10
del_exports_after_minutes: int = 5
demo_data_path: DirectoryPath = Path(realpath(__file__)).parent.parent / "demo"

@computed_field
@property
def demo_data_path(self) -> str:
return Path(realpath(__file__)).parent.parent / "demo"


class TekstConfig(BaseSettings):
Expand Down
Loading