diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bd03d50 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,49 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "Existing Docker Compose (Extend)", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you + // need/want to make. It has been commented out because the project's + // compose.yml works as is. It's left here as something to be able to use + // later if you need it. + "dockerComposeFile": [ + "../compose.yml" + // "docker-compose.yml" + ], + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "app", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/app", + "features": { + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers-contrib/features/ruff:1": {}, + "ghcr.io/devcontainers/features/sshd:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..bbb4cdb --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,28 @@ +# This is not used in python-starter. It will be used if the +# ".docker-compose.yml" line is uncommented in devcontainer.json +version: '3.8' +services: + # Update this to the name of the service you want to work with in your docker-compose.yml file + app: + # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer + # folder. Note that the path of the Dockerfile and context is relative to the *primary* + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" + # array). The sample below assumes your primary file is in the root of your project. + # + # build: + # context: . + # dockerfile: .devcontainer/Dockerfile + + # volumes: + # # Update this to wherever you want VS Code to mount the folder of your project + # # - ..:/workspaces:cached + + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. + # cap_add: + # - SYS_PTRACE + # security_opt: + # - seccomp:unconfined + + # Overrides default command so things don't shut down after the process ends. + command: /bin/sh -c "while sleep 1000; do :; done" + diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1d17dae --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.venv diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index a0fabef..4c4b010 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,11 @@ .bash_history .python_history .cache/ +.pytest_cache +__pycache__ +.dotnet/ +.vscode-server +.local +.gitconfig +.venv +.ssh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b5b783b..686847b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,19 +8,35 @@ # I did not recommed using an alpine image because it lacks the package installer pip and the support for installing # wheel packages, which are both needed for installing applications like Pandas and Numpy. -# The base layer will contain the dependencies shared by the other layers +# The base layer contains the instruction for creating the app user, setting the +# working directory, setting an "always on" command. FROM python:3.11-slim-bookworm as base # Allowing the argumenets to be read into the dockerfile. Ex: .env > compose.yml > Dockerfile ARG POETRY_VERSION -# true = development / false = production -ARG DEV +ARG UID=1000 +ARG GID=1000 + + +# Create the user and usergroup +RUN groupadd -g ${GID} -o app +RUN useradd -m -d /app -u ${UID} -g ${GID} -o -s /bin/bash app + # Set the working directory to /app WORKDIR /app -# Use this page as a reference for python and poetry environment variables: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED -# Ensure the stdout and stderr streams are sent straight to terminal, then you can see the output of your application +CMD ["tail", "-f", "/dev/null"] + +# Both build and development need poetry, so it is its own step. +FROM base as poetry + +RUN pip install poetry==${POETRY_VERSION} + +# Use this page as a reference for python and poetry environment variables: +# https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED Ensure +# the stdout and stderr streams are sent straight to terminal, then you can see +# the output of your application ENV PYTHONUNBUFFERED=1\ # Avoid the generation of .pyc files during package install # Disable pip's cache, then reduce the size of the image @@ -34,49 +50,41 @@ ENV PYTHONUNBUFFERED=1\ POETRY_VIRTUALENVS_IN_PROJECT=1 \ POETRY_CACHE_DIR=/tmp/poetry_cache -RUN pip install poetry==${POETRY_VERSION} +# We want poetry on in development +FROM poetry as development -# Install the app. Just copy the files needed to install the dependencies -COPY pyproject.toml poetry.lock README.md ./ +# Switch to the non-root user "user" +USER app -# Poetry cache is used to avoid installing the dependencies every time the code changes, we will keep this folder in development environment and remove it in production -# --no-root, poetry will install only the dependencies avoiding to install the project itself, we will install the project in the final layer -# --without dev to avoid installing dev dependencies, we do not need test and linters in production environment -# --with dev to install dev dependencies, we need test and linters in development environment -# --mount, mount a folder for plugins with poetry cache, this will speed up the process of building the image -RUN if [ {${DEV}} ]; then \ - echo "Installing dev dependencies"; \ - poetry install --no-root --with dev \ - else \ - echo "Skipping dev dependencies"; \ - poetry install --no-root --without dev && rm -rf ${POETRY_CACHE_DIR}; \ - fi - -# Set up our final runtime layer -FROM python:3.11-slim-bookworm as runtime +# Below are production steps +FROM poetry as build -ARG UID=1000 -ARG GID=1000 +# Only copy the files needed to install the dependencies. Poetry requires +# README.md to exist in order to work +COPY pyproject.toml poetry.lock README.md ./ -# Create our users here in the last layer or else it will be lost in the previous discarded layers -# Create a system group named "app_user" with the -r flag -RUN groupadd -g ${GID} -o app -RUN useradd -m -d /app -u ${UID} -g ${GID} -o -s /bin/bash app +# Install the depdencies with Poetry. +# +# --no-root is used so that poetry will install only the dependencies not the project itself +# +# --without dev to avoid installing dev dependencies +# +# Poetry cache is used to avoid installing the dependencies every time the code +# changes. We delete this folder after installing. +RUN poetry install --no-root --without dev && rm -rf ${POETRY_CACHE_DIR}; + +# We do not need poetry in production. We will copy dependencies from the build +# step. +FROM base as production RUN mkdir -p /venv && chown ${UID}:${GID} /venv -RUN which pip && sleep 10 -# By adding /venv/bin to the PATH the dependencies in the virtual environment +# By adding /venv/bin to the PATH, the dependencies in the virtual environment # are used ENV VIRTUAL_ENV=/venv \ PATH="/venv/bin:$PATH" -COPY --chown=${UID}:${GID} --from=base "/app/.venv" ${VIRTUAL_ENV} - -# Switch to the non-root user "user" -USER app - -WORKDIR /app - COPY --chown=${UID}:${GID} . /app +COPY --chown=${UID}:${GID} --from=build "/app/.venv" ${VIRTUAL_ENV} -CMD ["tail", "-f", "/dev/null"] +# Switch to the app user +USER app \ No newline at end of file diff --git a/compose.yml b/compose.yml index 233dd81..0fa025a 100644 --- a/compose.yml +++ b/compose.yml @@ -2,7 +2,7 @@ services: app: build: context: . - target: runtime + target: development dockerfile: Dockerfile args: UID: ${UID:-1000} diff --git a/pyproject.toml b/pyproject.toml index 905559b..e72ddb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "python-starter" version = "0.1.0" description = "Boilerplate code for developing a python app in docker" -authors = ["Monique Rio , Samuel Sciolla , Lianet Sepulveda Torres , "] +authors = [ "Monique Rio , Samuel Sciolla , Lianet Sepulveda Torres "] readme = "README.md" packages = [{include = "python_starter"}] diff --git a/python_starter/__init__.py b/python_starter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python_starter/sample.py b/python_starter/sample.py new file mode 100644 index 0000000..af5e1d3 --- /dev/null +++ b/python_starter/sample.py @@ -0,0 +1,6 @@ +class Sample: + def __init__(self): + pass + + def add_one(self, a): + return(a + 1) diff --git a/python_starter/test_sample.py b/python_starter/test_sample.py new file mode 100644 index 0000000..78e3000 --- /dev/null +++ b/python_starter/test_sample.py @@ -0,0 +1,5 @@ +# Remove this file if you want tests in a separate tests directory +from python_starter.sample import Sample + +def test_add_one(): + assert Sample().add_one(21) == 22 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_sample.py b/tests/test_sample.py new file mode 100644 index 0000000..e1412a7 --- /dev/null +++ b/tests/test_sample.py @@ -0,0 +1,6 @@ +# Remove this tests directory if you want to have tests inline with production +# code +from python_starter.sample import Sample + +def test_add_one(): + assert Sample().add_one(21) == 22 \ No newline at end of file