diff --git a/{{cookiecutter.project_slug}}/.dockerignore b/{{cookiecutter.project_slug}}/.dockerignore new file mode 100644 index 0000000..fbaba3d --- /dev/null +++ b/{{cookiecutter.project_slug}}/.dockerignore @@ -0,0 +1,14 @@ +./tests +./scripts +.ruff_cache +.coveragerc +.dockerignore +.git +.gitignore +.pre-commit-config.yaml +dev-requirements.in +dev-requirements.txt +.profile +Dockerfile +requirements.in +tasks.py diff --git a/{{cookiecutter.project_slug}}/.drone.yml b/{{cookiecutter.project_slug}}/.drone.yml index 8ad623e..e7b6a70 100644 --- a/{{cookiecutter.project_slug}}/.drone.yml +++ b/{{cookiecutter.project_slug}}/.drone.yml @@ -20,3 +20,26 @@ trigger: # Secrets used to pull private images image_pull_secrets: - dockerconfigjson + +--- +kind: pipeline +type: docker +name: Build Production Image +steps: +- name: Build {{ cookiecutter.project_slug }} Container Image + image: plugins/docker + settings: + username: automate + password: + from_secret: automate_password + dockerfile: Dockerfile + registry: registry.runcible.io + repo: registry.runcible.io/{{ cookiecutter.project_slug }} + tags: + - ${DRONE_COMMIT_SHA} + when: + branch: + - master + event: + - push +--- \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/Dockerfile b/{{cookiecutter.project_slug}}/Dockerfile new file mode 100644 index 0000000..babd0ba --- /dev/null +++ b/{{cookiecutter.project_slug}}/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/{{cookiecutter.project_slug}}/entrypoint.sh b/{{cookiecutter.project_slug}}/entrypoint.sh new file mode 100755 index 0000000..42aa2b7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/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/{{cookiecutter.project_slug}}/tasks.py b/{{cookiecutter.project_slug}}/tasks.py index e421b5b..e4e5adc 100644 --- a/{{cookiecutter.project_slug}}/tasks.py +++ b/{{cookiecutter.project_slug}}/tasks.py @@ -1,5 +1,8 @@ +import os from invoke import task +IMAGE_RESPOSITORY = os.environ.get("IMAGE_RESPOSITORY", "registry.runcible.io/{{ cookiecutter.project_slug }}") + @task def update_deps(c): @@ -31,3 +34,18 @@ def delint(c): def build(c): """Builds the project as a Python package.""" c.run("python3 -m build") + + +@task +def build_image(c, dev=True, registry_user=None, registry_token=None, push=False, login=False): + """Builds the {{ cookiecutter.project_slug }} container image.""" + context_dir = c.run("pwd", hide=True).stdout.strip() + commit_sha = c.run("git rev-parse --short HEAD", hide=True).stdout.strip() + image_name = f"{IMAGE_RESPOSITORY}:{commit_sha}{'-dev' if dev else ''}" + c.run(f"docker build --build-arg='COMMIT_SHA={commit_sha}' -t {image_name} {context_dir}") + if login: + if registry_user is None or registry_token is None: + raise ValueError("--registry_user and --registry_token must be provided if using --login parameter") + c.run(f"docker login -u {registry_user} -p {registry_token}") + if push: + c.run(f"docker push {image_name}")