From 8547434c311187fb7a503c2eff2b40426984464c Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Thu, 30 Nov 2023 13:22:21 -0500 Subject: [PATCH] Dockerfile support --- .dockerignore | 15 +++++++++++ Dockerfile | 55 +++++++++++++++++++++++++++++++++++++++ entrypoint.sh | 8 ++++++ speech_collect/app.py | 15 +++++++++++ speech_collect/example.py | 5 ---- tasks.py | 24 +++++++++++++++++ tests/test_app.py | 0 tests/test_example.py | 6 ----- 8 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100755 entrypoint.sh create mode 100644 speech_collect/app.py delete mode 100644 speech_collect/example.py create mode 100644 tasks.py create mode 100644 tests/test_app.py delete mode 100644 tests/test_example.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..264cddd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +./tests +./scripts +.ruff_cache +.coveragerc +.dockerignore +.git +.gitignore +.pre-commit-config.yaml +dev-requirements.in +dev-requirements.txt +pyproject.toml +.profile +Dockerfile +requirements.in +tasks.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ad21914 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# syntax = docker/dockerfile:1.4 + +# Best practice: Choose a stable base image and tag. +FROM python:3.11-slim-bookworm + +# Install security updates, and some useful packages. +# +# Best practices: +# * Make sure apt-get doesn't run in interactive mode. +# * Update system packages. +# * Pre-install some useful tools. +# * Minimize system package installation. +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y --no-install-recommends tini procps net-tools && \ + apt-get -y clean && \ + rm -rf /var/lib/apt/lists/* + +# Install dependencies. +# +# Best practices: +# * `COPY` in files only when needed. +# * Reduce disk usage from `pip` installs. +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Create a new user to run as. +# +# Best practices: Don't run as root. +RUN useradd --create-home appuser +USER appuser +WORKDIR /home/appuser + +# Copy in the code. +# +# Best practices: Avoid extra chowns. +COPY --chown=appuser . . + +# Best practices: Prepare for C crashes. +ENV PYTHONFAULTHANDLER=1 +ENV PYTHONUNBUFFERED=0 + +ARG COMMIT_SHA + +LABEL io.runcible.repo-sha="${COMMIT_SHA}" + +# Run the code when the image is run: +# +# Best practices: +# * Add an `init` process. +# * Make sure images shut down correctly (via ENTRYPOINT [] syntax). +# * '-g' option means killing the container kills all processes, not just the +# entrypoint shell. +ENTRYPOINT ["tini", "-g", "--", "./entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..42aa2b7 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Best practice: Bash strict mode. +set -euo pipefail + +# Best practice: Make sure the image shuts down correctly by using `exec` in +# entry point shell scripts. +exec "$@" diff --git a/speech_collect/app.py b/speech_collect/app.py new file mode 100644 index 0000000..0b18a21 --- /dev/null +++ b/speech_collect/app.py @@ -0,0 +1,15 @@ +from litestar import Litestar +from litestar import get + + +@get("/") +async def index() -> str: + return "Hello, world!" + + +@get("/books/{book_id:int}") +async def get_book(book_id: int) -> dict[str, int]: + return {"book_id": book_id} + + +app = Litestar([index, get_book]) diff --git a/speech_collect/example.py b/speech_collect/example.py deleted file mode 100644 index b83ab56..0000000 --- a/speech_collect/example.py +++ /dev/null @@ -1,5 +0,0 @@ -class Example: - """An example class""" - - def __init__(self, name): - self.name = name diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..6fe2666 --- /dev/null +++ b/tasks.py @@ -0,0 +1,24 @@ +from invoke import task + + +@task +def serve(c): + """Serves the speech-collect application locally.""" + c.run("LITESTAR_APP=speech_collect.app:app litestar run --port 8888 --host 0.0.0.0 --reload") + + +@task +def build_image(c, dev=True): + """Builds the speech-collect container image.""" + context_dir = c.run("pwd", hide=True).stdout.strip() + commit_sha = c.run("git rev-parse --short HEAD", hide=True).stdout.strip() + c.run( + f"docker build --build-arg='COMMIT_SHA={commit_sha}' -t speech-collect:{commit_sha}{'-dev' if dev else ''} {context_dir}" + ) + + +@task +def delint(c): + """Applies automated linters to project""" + c.run("isort ./speech_collect ./tests", pty=True) + c.run("black ./speech_collect ./tests", pty=True) diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_example.py b/tests/test_example.py deleted file mode 100644 index 3ed5f0e..0000000 --- a/tests/test_example.py +++ /dev/null @@ -1,6 +0,0 @@ -from speech_collect.example import Example - - -def test_example(): - my_example = Example(name="dirp") - assert my_example.name == "dirp"