-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathContainerfile
More file actions
125 lines (105 loc) · 5.44 KB
/
Copy pathContainerfile
File metadata and controls
125 lines (105 loc) · 5.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# ==============================================================================
# 0. THE ASTRAL PLANE
# ==============================================================================
# We define an alias 'uv' for the image containing the uv binary.
# This allows us to "borrow" the tool later without downloading it into our layers.
FROM ghcr.io/astral-sh/uv:0.11.28 AS uv
# ==============================================================================
# STAGE I: BUILDER
# ==============================================================================
FROM python:3.13-slim-bookworm AS builder
# Configure uv for container usage:
# - LINK_MODE=copy: Essential for cache mounts (hardlinks fail across filesystems).
# - COMPILE_BYTECODE: Compiles .pyc files for faster startup.
# - PYTHON_DOWNLOADS=0: Use the system python, don't download a managed one.
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=0 \
UV_NO_DEV=1
WORKDIR /app
# --- 1. Install Dependencies (Cached Layer) ---
RUN --mount=from=uv,source=/uv,target=/bin/uv \
--mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
/bin/uv sync --frozen --no-dev --no-install-project --no-editable
# --- 2. Install Project (Frequent Change Layer) ---
COPY pyproject.toml uv.lock README.md LICENSE THIRD_PARTY_NOTICES.md ./
COPY scripts/generate_python_third_party_notices.py ./scripts/
COPY src ./src
# Install the project non-editably so the environment does not point back to /app/src.
RUN --mount=from=uv,source=/uv,target=/bin/uv \
--mount=type=cache,target=/root/.cache/uv \
/bin/uv sync --frozen --no-dev --no-editable
# Inventory the exact Python environment that will cross the image boundary. The
# pure-Python psycopg package uses the runner's system libpq; its bundled-binary
# distribution remains forbidden until its transitive native payload is audited.
RUN /app/.venv/bin/python scripts/generate_python_third_party_notices.py \
--output /app/PYTHON_THIRD_PARTY_NOTICES.txt \
--forbid-distribution psycopg-binary
# ==============================================================================
# STAGE II: RUNNER (The Production Artifact)
# ==============================================================================
FROM python:3.13-slim-bookworm
ARG VCS_REF=unknown
LABEL org.opencontainers.image.source="https://github.com/hexanomicon/lychd" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.licenses="MPL-2.0"
# Psycopg deliberately uses the pure-Python implementation plus Debian's
# dynamically loaded libpq. Debian retains the package copyright record under
# /usr/share/doc/libpq5/copyright.
RUN apt-get update && \
apt-get install --yes --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
# --- Layer 1: The Prisoner (Identity Setup) ---
# We create a dedicated, unprivileged system user.
# Reference: ADR 09 [Security] - Layer 1.
RUN groupadd --system --gid 1001 lich && \
useradd --system --uid 1001 --gid 1001 --create-home --home-dir /home/lich lich
# --- Geography (XDG Standards) ---
# We establish Path Symmetry (ADR 13). Regardless of the UID running the process,
# the application logic always looks for its soul in /home/lich.
ENV HOME=/home/lich \
XDG_CONFIG_HOME=/home/lich/.config \
XDG_DATA_HOME=/home/lich/.local/share \
PATH="/app/.venv/bin:$PATH" \
LITESTAR_APP="lychd.app:create_app"
WORKDIR /app
# --- The Transplant ---
COPY --from=builder --chown=lich:lich /app/.venv /app/.venv
COPY --from=builder --chown=lich:lich /app/LICENSE /app/LICENSE
COPY --from=builder --chown=lich:lich /app/THIRD_PARTY_NOTICES.md /app/THIRD_PARTY_NOTICES.md
COPY --from=builder --chown=lich:lich /app/PYTHON_THIRD_PARTY_NOTICES.txt /app/PYTHON_THIRD_PARTY_NOTICES.txt
# --- Layer 4: THE GREAT SEAL (Immutability) ---
# We strip write access (-w) from the entire /app directory.
# Reference: ADR 09 [Security] - Layer 4.
# This ensures that even the 'Magus' (User 1000) cannot modify the Vessel's brain at runtime.
RUN chmod -R a-w /app
# --- Domain and Sphere Preparation ---
# We create the skeletal structure of the Crypt and Codex.
RUN mkdir -p /home/lich/.config/lychd \
/home/lich/.local/share/lychd/lab \
/home/lich/.local/share/lychd/extensions \
/home/lich/library \
/home/lich/work
# --- THE PERMISSION BRIDGE (Agnosticism) ---
# We make the internal home directory world-writable (777).
#
# WHY? Identity Symmetry (ADR 08/09).
# At runtime, the Rune Scribe overrides the user to match the host Magus (UID 1000).
# If this directory were hard-owned by 'lich' (1001), the Magus (1000) would be
# locked out of the internal skeleton before host volumes are mounted.
# 777 ensures the "Suit of Armor" fits any UID that steps into it.
RUN chmod -R 777 /home/lich && \
chown -R lich:lich /home/lich
# --- Layer 1: The Fail-Secure Default ---
# Reference: ADR 09 [Security].
# By default, we run as 'lich' (1001).
# 1. If run manually (GHCR): Runs as 1001. Non-root, but "Unbound" from host files.
# 2. If run via LychD Rune: The Quadlet 'User=%U' overrides this to UID 1000.
# Combined with 'keep-id', we achieve the "Double Non-Root Bridge."
USER lich
# The threshold of the Sepulcher.
EXPOSE 8000
# The Final Awakening.
CMD ["granian", "--interface", "asgi", "--factory", "--workers", "1", "--host", "0.0.0.0", "--port", "8000", "lychd.app:create_app"]