|
| 1 | +# syntax=docker/dockerfile:1.10 |
| 2 | +# check=error=true |
| 3 | + |
| 4 | +# NB: The above comments are special directives to Docker that enable us to use |
| 5 | +# more up-to-date Dockerfile syntax and will cause the build to fail if any |
| 6 | +# Docker build checks fail: |
| 7 | +# https://docs.docker.com/reference/build-checks/ |
| 8 | +# |
| 9 | +# We've set it so that failing checks will cause `docker build .` to fail, but |
| 10 | +# when that happens the error message isn't very helpful. To get more |
| 11 | +# information, run `docker build --check .` instead. |
| 12 | + |
| 13 | +# Build stage hierarchy: |
| 14 | +# |
| 15 | +# ┌────────┐ ┌──────────────┐ |
| 16 | +# │ base │ │ frontend-* │ |
| 17 | +# └────────┘ └──────────────┘ |
| 18 | +# / \ / |
| 19 | +# ┌───────┐ ┌───────┐ |
| 20 | +# │ dev │ │ web │ |
| 21 | +# └───────┘ └───────┘ |
| 22 | +# \ |
| 23 | +# ┌──────┐ |
| 24 | +# │ ci │ |
| 25 | +# └──────┘ |
| 26 | + |
| 27 | +############## |
| 28 | +# base stage # |
| 29 | +############## |
| 30 | + |
| 31 | +# This stage is the base for the web and dev stages. It contains the version of |
| 32 | +# Python we want to use and any OS-level dependencies that we need in all |
| 33 | +# environments. It also sets up the always-activated virtual environment and |
| 34 | +# installs uv. |
| 35 | + |
| 36 | +FROM python:3.12-slim-bookworm AS base |
| 37 | + |
| 38 | +WORKDIR /app |
| 39 | + |
| 40 | +# Install common OS-level dependencies |
| 41 | +RUN --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ |
| 42 | + --mount=type=cache,target=/var/cache/apt,sharing=locked \ |
| 43 | + <<EOF |
| 44 | + apt-get --quiet --yes update |
| 45 | + apt-get --quiet --yes install --no-install-recommends \ |
| 46 | + build-essential \ |
| 47 | + curl \ |
| 48 | + libpq-dev \ |
| 49 | + git |
| 50 | + apt-get --quiet --yes autoremove |
| 51 | +EOF |
| 52 | + |
| 53 | +# Create an unprivileged user and virtual environment for the app |
| 54 | +ARG UID=1000 |
| 55 | +ARG GID=1000 |
| 56 | +ARG USERNAME=hypha |
| 57 | +RUN <<EOF |
| 58 | + # Create the unprivileged user and group. If you have issues with file |
| 59 | + # ownership, you may need to adjust the UID and GID build args to match your |
| 60 | + # local user. |
| 61 | + groupadd --gid $GID $USERNAME |
| 62 | + useradd --gid $GID --uid $UID --create-home $USERNAME |
| 63 | + mkdir /app/.venv |
| 64 | + chown -R $UID:$GID /app |
| 65 | +EOF |
| 66 | + |
| 67 | +# Install uv. |
| 68 | +COPY --from=ghcr.io/astral-sh/uv:0.6.8 /uv /uvx /usr/local/bin |
| 69 | + |
| 70 | +# Enable bytecode compilation |
| 71 | +ENV UV_COMPILE_BYTECODE=1 |
| 72 | + |
| 73 | +# Copy from the cache instead of linking since it's a mounted volume |
| 74 | +ENV UV_LINK_MODE=copy |
| 75 | + |
| 76 | +# Set common environment variables |
| 77 | +ENV \ |
| 78 | + # Don't buffer Python output so that we don't lose logs in the event of a crash |
| 79 | + PYTHONUNBUFFERED=1 \ |
| 80 | + # Make sure the virtual environment's bin directory and uv are on the PATH |
| 81 | + PATH=/app/.venv/bin:/bin:$PATH |
| 82 | + |
| 83 | +# Install .bashrc for dj shortcuts |
| 84 | +COPY --chown=$UID:$GID ./docker/bashrc.sh ./docker/bashrc.sh |
| 85 | +RUN ln -sTf /app/docker/bashrc.sh /home/$USERNAME/.bashrc |
| 86 | + |
| 87 | +# Switch to the unprivileged user |
| 88 | +USER $USERNAME |
| 89 | + |
| 90 | +# Install the project's dependencies using the lockfile and settings |
| 91 | +RUN --mount=type=cache,target=/home/$USERNAME/.cache/uv,uid=$UID,gid=$GID \ |
| 92 | + --mount=type=bind,source=uv.lock,target=uv.lock \ |
| 93 | + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ |
| 94 | + uv sync --frozen --no-install-project --no-dev |
| 95 | + |
| 96 | +# Then, add the rest of the project source code and install it |
| 97 | +# Installing separately from its dependencies allows optimal layer caching |
| 98 | +ADD . /app |
| 99 | +RUN --mount=type=cache,target=/home/$USERNAME/.cache/uv,uid=$UID,gid=$GID \ |
| 100 | + uv sync --frozen --no-dev |
| 101 | + |
| 102 | + |
| 103 | +################### |
| 104 | +# frontend stages # |
| 105 | +################### |
| 106 | + |
| 107 | +FROM node:20-slim AS frontend-deps |
| 108 | + |
| 109 | +# This stage is used to install the front-end build dependencies. It's separate |
| 110 | +# from the frontend-build stage so that we can initialise the node_modules |
| 111 | +# volume in the dev container from here without needing to run the production |
| 112 | +# build. |
| 113 | + |
| 114 | +WORKDIR /build/ |
| 115 | + |
| 116 | +# Make any build & post-install scripts that respect this variable behave as if |
| 117 | +# we were in a CI environment (e.g. for logging verbosity purposes) |
| 118 | +ENV CI=true |
| 119 | + |
| 120 | +# Install front-end dependencies |
| 121 | +COPY package.json package-lock.json ./ |
| 122 | +RUN --mount=type=cache,target=/root/.npm \ |
| 123 | + npm ci --no-audit --progress=false |
| 124 | + |
| 125 | + |
| 126 | +FROM frontend-deps AS frontend-build |
| 127 | + |
| 128 | +# This stage is used to compile the front-end assets. The web stage copies the |
| 129 | +# compiled assets bundles from here, so it doesn't need to have the front-end |
| 130 | +# build dependencies installed. |
| 131 | + |
| 132 | +# Compile static files |
| 133 | +COPY .stylelintrc.yaml ./ |
| 134 | +COPY ./hypha/ ./hypha/ |
| 135 | +RUN npm run build |
| 136 | + |
| 137 | + |
| 138 | +############# |
| 139 | +# web stage # |
| 140 | +############# |
| 141 | + |
| 142 | +# This is the stage that actually gets run in staging and production. |
| 143 | +# It extends the base stage by installing production Python dependencies and |
| 144 | +# copying in the compiled front-end assets. It runs the WSGI server, gunicorn, |
| 145 | +# in its CMD. |
| 146 | + |
| 147 | +FROM base AS web |
| 148 | + |
| 149 | +# Set production environment variables |
| 150 | +ENV \ |
| 151 | + # Django settings module |
| 152 | + DJANGO_SETTINGS_MODULE=hypha.settings.production \ |
| 153 | + # Default port and number of workers for gunicorn to spawn |
| 154 | + PORT=8000 \ |
| 155 | + WEB_CONCURRENCY=2 |
| 156 | + |
| 157 | +# Copy in built static files and the application code. Run collectstatic so |
| 158 | +# whitenoise can serve static files for us. |
| 159 | +COPY . . |
| 160 | +ARG UID |
| 161 | +ARG GID |
| 162 | +COPY --chown=$UID:$GID --from=frontend-build --link /build/hypha/static_compiled ./hypha/static_compiled |
| 163 | +RUN <<EOF |
| 164 | + python -m django collectstatic --noinput --clear |
| 165 | +EOF |
| 166 | + |
| 167 | +# Run Gunicorn using the config in gunicorn.conf.py (the default location for |
| 168 | +# the config file). To change gunicorn settings without needing to make code |
| 169 | +# changes and rebuild this image, set the GUNICORN_CMD_ARGS environment variable. |
| 170 | +CMD ["gunicorn"] |
| 171 | + |
| 172 | + |
| 173 | +############# |
| 174 | +# dev stage # |
| 175 | +############# |
| 176 | + |
| 177 | +# This stage is used in the development environment, either via `fab sh` etc. or |
| 178 | +# as the dev container in VS Code or PyCharm. It extends the base stage by |
| 179 | +# adding additional OS-level dependencies to allow things like using git and |
| 180 | +# psql. It also adds sudo and gives the unprivileged user passwordless sudo |
| 181 | +# access to make things like experimenting with different OS dependencies easier |
| 182 | +# without needing to rebuild the image or connect to the container as root. |
| 183 | +# |
| 184 | +# This stage does not include the application code at build time! Including the |
| 185 | +# code would result in this image needing to be rebuilt every time the code |
| 186 | +# changes at all which is unnecessary because we always bind mount the code at |
| 187 | +# /app/ anyway. |
| 188 | + |
| 189 | +FROM base AS dev |
| 190 | + |
| 191 | +# Switch to the root user and Install extra OS-level dependencies for |
| 192 | +# development, including Node.js and the correct version of the Postgres client |
| 193 | +# library (Debian's bundled version is normally too old) |
| 194 | +USER root |
| 195 | +ARG POSTGRES_VERSION=16 |
| 196 | +RUN --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ |
| 197 | + --mount=type=cache,target=/var/cache/apt,sharing=locked \ |
| 198 | + <<EOF |
| 199 | + apt-get --quiet --yes update |
| 200 | + apt-get --quiet --yes install \ |
| 201 | + git \ |
| 202 | + gnupg \ |
| 203 | + less \ |
| 204 | + openssh-client \ |
| 205 | + postgresql-common \ |
| 206 | + sudo |
| 207 | + # Install the Postgres repo |
| 208 | + /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y |
| 209 | + # Intall the Postgres client (make sure the version matches the one in production) |
| 210 | + apt-get --quiet --yes install \ |
| 211 | + postgresql-client-${POSTGRES_VERSION} |
| 212 | + # Download and import the Nodesource GPG key |
| 213 | + mkdir -p /etc/apt/keyrings |
| 214 | + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg |
| 215 | + # Create NodeSource repository |
| 216 | + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list |
| 217 | + # Update lists again and install Node.js |
| 218 | + apt-get --quiet --yes update |
| 219 | + apt-get --quiet --yes install nodejs |
| 220 | + # Tidy up |
| 221 | + apt-get --quiet --yes autoremove |
| 222 | +EOF |
| 223 | + |
| 224 | +# Give the unprivileged user passwordless sudo access |
| 225 | +ARG USERNAME |
| 226 | +RUN echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers |
| 227 | + |
| 228 | +# Make less the default pager for things like psql results and git logs |
| 229 | +ENV PAGER=less |
| 230 | + |
| 231 | +# Flag that this is the dev container |
| 232 | +ENV DEVCONTAINER=1 |
| 233 | + |
| 234 | +# Switch back to the unprivileged user |
| 235 | +USER $USERNAME |
| 236 | + |
| 237 | +# Copy in the node_modules directory from the frontend-deps stage to initialise |
| 238 | +# the volume that gets mounted here |
| 239 | +ARG UID |
| 240 | +ARG GID |
| 241 | +COPY --chown=$UID:$GID --from=frontend-deps --link /build/node_modules ./node_modules |
| 242 | + |
| 243 | +# Install the dev dependencies (they're omitted in the base stage) |
| 244 | +RUN --mount=type=cache,target=/home/$USERNAME/.cache/uv,uid=$UID,gid=$GID \ |
| 245 | + uv sync --frozen --dev |
| 246 | + |
| 247 | +# Just do nothing forever - exec commands elsewhere |
| 248 | +CMD ["tail", "-f", "/dev/null"] |
| 249 | + |
| 250 | + |
| 251 | +############ |
| 252 | +# ci stage # |
| 253 | +############ |
| 254 | + |
| 255 | +FROM dev AS ci |
| 256 | + |
| 257 | +# This stage is used in the CI pipeline to run tests, linters, etc. |
| 258 | +# It extends the dev stage by adding in the collected static files from the web |
| 259 | +# stage so that we can use Whitenoise's manifest storage backend in tests |
| 260 | +# without needing to run collectstatic. |
| 261 | + |
| 262 | +ARG UID |
| 263 | +ARG GID |
| 264 | +COPY --chown=$UID:$GID --from=web --link /app/static/ /app/static/ |
0 commit comments