Skip to content

Commit

Permalink
Add Docker setup
Browse files Browse the repository at this point in the history
  • Loading branch information
zerolab committed Oct 17, 2024
1 parent fb1c835 commit 02632d3
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .docker/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
web: python manage.py runserver 0.0.0.0:8000
scheduler: python manage.py scheduler
frontend: npm run start:reload
10 changes: 10 additions & 0 deletions .docker/bashrc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Note: This file is loaded on all environments, even production.

alias dj="django-admin"

if [ -n "$DEVCONTAINER" ]
then
alias djrun="django-admin runserver 0.0.0.0:8000"
alias djrunplus="python manage.py runserver_plus 0.0.0.0:8000"
alias honcho="honcho -f docker/Procfile"
fi
50 changes: 50 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/Dockerfile
/docker-compose.yml
/docker-compose.override.yml.example

.git
**/__pycache__
*.pyc
.DS_Store
*.swp
/venv/
/.venv/
/static/
/media/
/tmp/
/.vagrant/
/Vagrantfile.local
node_modules/
coverage
/npm-debug.log
/.idea/
/.devcontainer/
/.mypy_cache/

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

/database_dumps

/ons/static_compiled
/ons/settings/local.py

/ons/jinja2/assets/styles/print.css
/ons/jinja2/components
/ons/jinja2/layout
262 changes: 262 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# syntax=docker/dockerfile:1.10
# check=error=true

# NB: The above comments are special directives to Docker that enable us to use
# more up-to-date Dockerfile syntax and will cause the build to fail if any
# Docker build checks fail:
# https://docs.docker.com/reference/build-checks/
#
# We've set it so that failing checks will cause `docker build .` to fail, but
# when that happens the error message isn't very helpful. To get more
# information, run `docker build --check .` instead.

# Build stage hierarchy:
#
# ┌────────┐ ┌──────────────┐
# │ base │ │ frontend-* │
# └────────┘ └──────────────┘
# / \ /
# ┌───────┐ ┌───────┐ ┌───────────┐
# │ dev │ │ web │ - │ release │
# └───────┘ └───────┘ └───────────┘
#

##############
# base stage #
##############

# This stage is the base for the web and dev stages. It contains the version of
# Python we want to use and any OS-level dependencies that we need in all
# environments. It also sets up the always-activated virtual environment and
# installs Poetry.

FROM python:3.12-slim AS base

WORKDIR /app

# Install common OS-level dependencies
RUN --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
<<EOF
apt --quiet --yes update
apt --quiet --yes install --no-install-recommends \
build-essential \
curl \
libpq-dev \
git \
jq \
unzip \
gettext \
&& apt --quiet --yes autoremove
EOF

# Create an unprivileged user and virtual environment for the app
ARG UID=1000
ARG GID=1000
ARG USERNAME=ons
ARG VIRTUAL_ENV=/venv
RUN <<EOF
# Create the unprivileged user and group. If you have issues with file
# ownership, you may need to adjust the UID and GID build args to match your
# local user.
groupadd --gid $GID $USERNAME
useradd --gid $GID --uid $UID --create-home $USERNAME
python -m venv --upgrade-deps $VIRTUAL_ENV
chown -R $UID:$GID /app $VIRTUAL_ENV
EOF

# Install Poetry in its own virtual environment
ARG POETRY_VERSION=1.8.3
ARG POETRY_HOME=/opt/poetry
RUN --mount=type=cache,target=/root/.cache/pip <<EOF
python -m venv --upgrade-deps $POETRY_HOME
$POETRY_HOME/bin/pip install poetry==$POETRY_VERSION
EOF

# Set common environment variables
ENV \
# Make sure the project code is always importable
PYTHONPATH=/app \
# Don't buffer Python output so that we don't lose logs in the event of a crash
PYTHONUNBUFFERED=1 \
# Let things know that a virtual environment is being used
VIRTUAL_ENV=$VIRTUAL_ENV \
# Make sure the virtual environment's bin directory and Poetry are on the PATH
PATH=$VIRTUAL_ENV/bin:$POETRY_HOME/bin:$PATH

# Install .bashrc for dj shortcuts
COPY --chown=$UID:$GID ./.docker/bashrc.sh ./.docker/bashrc.sh
RUN ln -sTf /app/.docker/bashrc.sh /home/$USERNAME/.bashrc

# Switch to the unprivileged user
USER $USERNAME

# Install the app's production dependencies. That prevents us
# needing to reinstall all the dependencies every time the app code changes.
COPY pyproject.toml poetry.lock ./
RUN --mount=type=cache,target=/home/$USERNAME/.cache/,uid=$UID,gid=$GID \
<<EOF
# Install the production dependencies
poetry install --no-root --without dev
EOF


###################
# frontend stages #
###################

FROM node:20-slim AS frontend-deps

# This stage is used to install the front-end build dependencies. It's separate
# from the frontend-build stage so that we can initialise the node_modules
# volume in the dev container from here without needing to run the production
# build.

WORKDIR /build/

# Make any build & post-install scripts that respect this variable behave as if
# we were in a CI environment (e.g. for logging verbosity purposes)
ENV CI=true

# Install front-end dependencies
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=optional --no-audit --progress=false


FROM frontend-deps AS frontend-build

# This stage is used to compile the front-end assets. The web stage copies the
# compiled assets bundles from here, so it doesn't need to have the front-end
# build dependencies installed.

# Compile static files
COPY .eslintignore .eslintrc.js .stylelintrc.js tsconfig.json webpack.config.js ./
COPY ./ons/static_src/ ./ons/static_src/
RUN npm run build:prod


#############
# web stage #
#############

# This is the stage that actually gets run in staging and production on Heroku.
# It extends the base stage by installing production Python dependencies and
# copying in the compiled front-end assets. It runs the WSGI server, gunicorn,
# in its CMD.

FROM base AS web

# Set production environment variables
ENV \
# Django settings module
DJANGO_SETTINGS_MODULE=ons.settings.production \
# Default port and number of workers for gunicorn to spawn
PORT=8000 \
WEB_CONCURRENCY=2

# Copy in built static files and the application code. Run collectstatic so
# whitenoise can serve static files for us.
COPY . .
ARG UID
ARG GID
COPY --chown=$UID:$GID --from=frontend-build --link /build/ons/static_compiled ./ons/static_compiled
RUN <<EOF
django-admin collectstatic --noinput --clear
EOF

# Run Gunicorn using the config in gunicorn.conf.py (the default location for
# the config file). To change gunicorn settings without needing to make code
# changes and rebuild this image, set the GUNICORN_CMD_ARGS environment variable.
CMD ["gunicorn"]


#################
# release stage #
#################

# This stage is run in the relase phase. It's the same as the web
# stage, it just overrides CMD to run deployment checks and migrations.

FROM web AS release

SHELL ["/bin/bash", "-c"]
CMD django-admin check --deploy && django-admin createcachetable && django-admin migrate --noinput


#############
# dev stage #
#############

# This stage is used in the development environment, either via `fab sh` etc. or
# as the dev container in VS Code or PyCharm. It extends the base stage by
# adding additional OS-level dependencies to allow things like using git and
# psql. It also adds sudo and gives the unprivileged user passwordless sudo
# access to make things like experimenting with different OS dependencies easier
# without needing to rebuild the image or connect to the container as root.
#
# This stage does not include the application code at build time! Including the
# code would result in this image needing to be rebuilt every time the code
# changes at all which is unnecessary because we always bind mount the code at
# /app/ anyway.

FROM base AS dev

# Switch to the root user and Install extra OS-level dependencies for
# development, including Node.js and the correct version of the Postgres client
# library (Debian's bundled version is normally too old)
USER root
ARG POSTGRES_VERSION=16
RUN --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
<<EOF
apt --quiet --yes update
apt --quiet --yes install \
git \
gnupg \
less \
openssh-client \
postgresql-common \
sudo
# Install the Postgres repo
/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
# Intall the Postgres client (make sure the version matches the one in production)
apt --quiet --yes install \
postgresql-client-${POSTGRES_VERSION}
# Download and import the Nodesource GPG key
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
# Create NodeSource repository
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
# Update lists again and install Node.js
apt --quiet --yes update
apt --quiet --yes install nodejs
# Tidy up
apt --quiet --yes autoremove
EOF

# Give the unprivileged user passwordless sudo access
ARG USERNAME
RUN echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Make less the default pager for things like psql results and git logs
ENV PAGER=less

# Flag that this is the dev container
ENV DEVCONTAINER=1

# Switch back to the unprivileged user
USER $USERNAME

# Copy in the node_modules directory from the frontend-deps stage to initialise
# the volume that gets mounted here
ARG UID
ARG GID
COPY --chown=$UID:$GID --from=frontend-deps --link /build/node_modules ./node_modules

# Install the dev dependencies (they're omitted in the base stage)
RUN --mount=type=cache,target=/home/$USERNAME/.cache/,uid=$UID,gid=$GID \
poetry install

# Just do nothing forever - exec commands elsewhere
CMD ["tail", "-f", "/dev/null"]
21 changes: 21 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,24 @@ megalint: ## Run the mega-linter.
load-design-system-templates: ## 🎨️ - Load the design system templates
./scripts/load_release.sh onsdigital/design-system $(DESIGN_SYSTEM_VERSION)
./scripts/finalize_design_system_setup.sh $(DESIGN_SYSTEM_VERSION)

.PHONY: docker-build
docker-build: ## Build Docker container
docker compose pull
docker compose build

.PHONY: docker-start
docker-start: ## Start Docker containers
docker compose up --detach

.PHONY: docker-stop
docker-stop: ## Stop Docker containers
docker compose stop

.PHONY: docker-shell
docker-shell: ## SSH into Docker container
docker compose exec web bash

.PHONY: docker-destroy
docker-destroy: ## Tear down the Docker containers
docker compose down --volumes
8 changes: 8 additions & 0 deletions docker-compose.override.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copy this file to docker-compose.override.yml and modify it to your needs

services:
web:
# Remove forward for port 3000 so Webpack's dev server can be run locally
ports: !override
- 8000:8000 # Django development server
- 8001:8001 # mkdocs server
Loading

0 comments on commit 02632d3

Please sign in to comment.