diff --git a/.dockerignore b/.dockerignore index db3149bac..3b52e2738 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,6 +7,10 @@ .coverage .eslintcache docker-compose-dev.yml +docker-compose.yml Makefile.custom.config *.log -data \ No newline at end of file +data + +**/__pycache__/ +**/*.py[cod] diff --git a/.env.docker b/.env.docker.dev similarity index 91% rename from .env.docker rename to .env.docker.dev index 7658d08c5..97f56cf37 100644 --- a/.env.docker +++ b/.env.docker.dev @@ -7,8 +7,8 @@ export FLASK_SKIP_DOTENV=1 export APP_SETTINGS=fittrackee.config.DevelopmentConfig export APP_SECRET_KEY='just for test' # export APP_WORKERS= -export APP_LOG=fittrackee.log -export UPLOAD_FOLDER=/usr/src/app/uploads +export APP_LOG=/usr/src/app/data/logs/fittrackee.log +export UPLOAD_FOLDER=/usr/src/app/data/uploads # PostgreSQL export DATABASE_URL=postgresql://fittrackee:fittrackee@fittrackee-db:5432/fittrackee diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 000000000..59f76453f --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,44 @@ +# Custom variables initialisation + +# Docker volumes +# export UPLOAD_DIR= +# export LOG_DIR= +# export DATABASE_DIR= +# export REDIS_DIR= + +# Application +export FLASK_APP=fittrackee +export FLASK_SKIP_DOTENV=1 +# export APP_PORT=5000 +export APP_SECRET_KEY='PLEASE CHANGE ME' +export APP_LOG=/usr/src/app/logs/fittrackee.log +export UPLOAD_FOLDER=/usr/src/app/uploads + +# PostgreSQL +export POSTGRES_USER=fittrackee +export POSTGRES_PASSWORD= +export POSTGRES_DB=fittrackee +export DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@fittrackee-db:5432/${POSTGRES_DB} +# export DATABASE_DISABLE_POOLING= + +# Redis (required for API rate limits and email sending) +export REDIS_URL=redis://redis:6379 + +# API rate limits +# export API_RATE_LIMITS="300 per 5 minutes" + +# Emails +export UI_URL= +export EMAIL_URL= +export SENDER_EMAIL= + +# Workouts +# export TILE_SERVER_URL= +# export STATICMAP_SUBDOMAINS= +# export MAP_ATTRIBUTION= +# export DEFAULT_STATICMAP=False + +# Weather +# available weather API providers: visualcrossing +# export WEATHER_API_PROVIDER= +# export WEATHER_API_KEY= \ No newline at end of file diff --git a/.github/workflows/.publish-docker-images.yml b/.github/workflows/.publish-docker-images.yml new file mode 100644 index 000000000..4fba846f1 --- /dev/null +++ b/.github/workflows/.publish-docker-images.yml @@ -0,0 +1,66 @@ +name: Create and publish a Docker image + +on: + push: + tags: ["v*"] + +env: + GITHUB_REGISTRY: ghcr.io + GITHUB_IMAGE_NAME: ${{ github.repository }} + +jobs: + push_to_registries: + name: Push Docker image to multiple registries + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: | + fittrackee/fittrackee + ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME}} + + - name: Build and push Docker images + id: push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/Dockerfile b/Dockerfile index aa6df7caf..bd48973a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,51 @@ -FROM python:3.10 +FROM node:23-alpine AS node-builder + +RUN mkdir -p /usr/src/app/fittrackee_client /usr/src/app/fittrackee +WORKDIR /usr/src/app/fittrackee_client + +ENV PATH=/usr/src/app/fittrackee_client/node_modules/.bin:$PATH +COPY fittrackee_client/package.json /usr/src/app/fittrackee_client/package.json +COPY fittrackee_client/yarn.lock /usr/src/app/fittrackee_client/yarn.lock +RUN yarn install --silent --network-timeout 300000 + +COPY fittrackee_client/. /usr/src/app/fittrackee_client +RUN yarn build + +FROM python:3.13-alpine AS python-builder + +RUN mkdir -p /usr/src/app/ +WORKDIR /usr/src/app/ + +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_NO_INTERACTION=1 \ + VIRTUAL_ENV=/opt/venv + +COPY pyproject.toml poetry.lock README.md /usr/src/app/ +COPY fittrackee/. /usr/src/app/fittrackee/ +RUN rm -rf /usr/src/app/fittrackee/tests + +RUN python3 -m venv $VIRTUAL_ENV && pip install --upgrade pip +RUN pip install poetry==1.8.5 && . $VIRTUAL_ENV/bin/activate && poetry install --only main --no-interaction --quiet + +FROM python:3.13-alpine AS runtime + +RUN apk add --no-cache tini + +RUN addgroup -g 1000 -S fittrackee && \ + adduser -H -D -u 1000 -S fittrackee -G fittrackee -# set working directory -RUN mkdir -p /usr/src/app WORKDIR /usr/src/app -# copy source files -COPY . /usr/src/app - -# install requirements -ENV VIRTUAL_ENV=/opt/venv -RUN python3 -m venv $VIRTUAL_ENV -ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN pip install --upgrade pip -RUN pip install poetry -RUN . $VIRTUAL_ENV/bin/activate && poetry install --no-interaction --quiet - -# run fittrackee server -COPY ./docker-entrypoint.sh /docker-entrypoint.sh -COPY ./docker/set-admin.sh /usr/bin/set-admin -RUN chmod +x /usr/bin/set-admin && chmod +x /docker-entrypoint.sh -ENTRYPOINT [ "/docker-entrypoint.sh" ] \ No newline at end of file +ENV VIRTUAL_ENV=/opt/venv PATH="/opt/venv/bin:$PATH" + +COPY --chown=fittrackee --from=python-builder /opt/venv "$VIRTUAL_ENV" +COPY --chown=fittrackee --from=python-builder /usr/src/app/fittrackee /usr/src/app/fittrackee +COPY --chown=fittrackee --from=node-builder /usr/src/app/fittrackee/dist /usr/src/app/fittrackee/dist +COPY --chown=fittrackee docker-entrypoint.sh /usr/src/app/ + +RUN chmod 555 /usr/src/app/docker-entrypoint.sh + +USER fittrackee + +ENTRYPOINT ["/sbin/tini", "--"] \ No newline at end of file diff --git a/Dockerfile-dev b/Dockerfile-dev new file mode 100644 index 000000000..62d2226a4 --- /dev/null +++ b/Dockerfile-dev @@ -0,0 +1,22 @@ +FROM python:3.13-slim + +# set working directory +RUN mkdir -p /usr/src/app/data/uploads /usr/src/app/data/logs +WORKDIR /usr/src/app + +# copy source files +COPY . /usr/src/app + +# install requirements +ENV VIRTUAL_ENV=/opt/venv +RUN python3 -m venv $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN pip install --upgrade pip +RUN pip install poetry +RUN . $VIRTUAL_ENV/bin/activate && poetry install --no-interaction --quiet + +# run fittrackee server and workers +COPY ./docker-entrypoint-dev.sh /docker-entrypoint.sh +COPY ./docker/set-admin.sh /usr/bin/set-admin +RUN chmod +x /usr/bin/set-admin && chmod +x /docker-entrypoint.sh +ENTRYPOINT [ "/docker-entrypoint.sh" ] \ No newline at end of file diff --git a/Makefile b/Makefile index b19c37597..34d6c1c05 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,8 @@ docker-lint-client: docker compose -f docker-compose-dev.yml exec fittrackee_client $(NPM) type-check docker-lint-python: docker-run - docker compose -f docker-compose-dev.yml exec fittrackee docker/lint-python.sh + docker compose -f docker-compose-dev.yml exec fittrackee mypy fittrackee + docker compose -f docker-compose-dev.yml exec fittrackee ruff check fittrackee e2e docker-logs: docker compose -f docker-compose-dev.yml logs --follow @@ -95,7 +96,7 @@ docker-set-admin: docker compose -f docker-compose-dev.yml exec fittrackee ftcli users update $(USERNAME) --set-admin true docker-shell: - docker compose -f docker-compose-dev.yml exec fittrackee docker/shell.sh + docker compose -f docker-compose-dev.yml exec fittrackee /bin/bash docker-stop: docker compose -f docker-compose-dev.yml stop @@ -114,7 +115,7 @@ docker-test-e2e: docker-run docker compose -f docker-compose-dev.yml exec fittrackee docker/test-e2e.sh $(PYTEST_ARGS) docker-test-python: docker-run - docker compose -f docker-compose-dev.yml exec fittrackee docker/test-python.sh $(PYTEST_ARGS) + docker compose -f docker-compose-dev.yml exec fittrackee pytest fittrackee $(PYTEST_ARGS) docker-type-check: echo 'Running mypy in docker...' diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 3f4dc2ce6..a4550c182 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,7 +1,10 @@ +# docker compose for evaluation and development only +# not suitable for production + services: fittrackee-db: container_name: fittrackee-db - image: postgres:13 + image: postgres:17-alpine ports: - "5435:5432" environment: @@ -18,11 +21,13 @@ services: fittrackee: container_name: fittrackee - build: . + build: + context: . + dockerfile: Dockerfile-dev ports: - "5000:5000" env_file: - - .env + - .env.docker.dev depends_on: fittrackee-db: condition: service_healthy @@ -32,7 +37,8 @@ services: condition: service_started volumes: - .:/usr/src/app - - ./data/uploads:/usr/src/app/uploads + - ./data/uploads:/usr/src/app/data/uploads + - ./data/logs:/usr/src/app/data/logs fittrackee_client: container_name: fittrackee_client @@ -54,7 +60,7 @@ services: redis: container_name: fittrackee-redis - image: "redis:latest" + image: "redis:7.4" hostname: redis ports: - "6379:6379" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..d03274aed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,97 @@ +# docker compose for production +# (minimal version: Docker Compose version 2.30.0) +# +# minimal application (for single user) only needs fittrackee and fittrackee-db containers. +# +# for multi-users application, uncomment the following containers: +# - fittrackee-workers for email sending (EMAIL_URL must be set in .env to enable emails) +# - fittrackee-redis container for API rate limits and email sending + +services: + fittrackee-db: + container_name: fittrackee-db + image: postgres:17-alpine + env_file: + - .env + volumes: + - ${DATABASE_DIR:-./data/db}:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 15s + retries: 3 + networks: + - internal_network + restart: unless-stopped + + fittrackee: + container_name: fittrackee + env_file: + - .env +# TODO: to update on next release +# image: fittrackee/fittrackee:v0.8.13 + build: . + volumes: + - ${UPLOAD_DIR:-./data/uploads}:/usr/src/app/uploads + - ${UPLOAD_LOG:-./data/logs}:/usr/src/app/logs + post_start: + - command: chown -R fittrackee:fittrackee /usr/src/app/uploads /usr/src/app/logs + user: root + ports: + - "${APP_PORT:-5000}:5000" + command: 'sh docker-entrypoint.sh' + depends_on: + fittrackee-db: + condition: service_healthy +# Uncomment the following lines for API rate limit and email sending +# fittrackee-redis: +# condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget --spider http://127.0.0.1:5000/api/ping || exit 1"] + interval: 5s + timeout: 15s + retries: 3 + networks: + - external_network + - internal_network + restart: unless-stopped + +# Uncomment the following lines for email sending +# fittrackee-workers: +# container_name: fittrackee-workers +# env_file: +# - .env +## TODO: to update on next release +## image: fittrackee/fittrackee:v0.8.13 +# build: . +# volumes: +# - ${UPLOAD_LOG:-./data/logs}:/usr/src/app/logs +# post_start: +# - command: chown -R fittrackee:fittrackee /usr/src/app/logs +# user: root +# command: "flask worker --processes 2 >> /usr/src/app/logs/dramatiq.log 2>&1" +# depends_on: +# fittrackee: +# condition: service_healthy +# networks: +# - internal_network +# - external_network +# restart: unless-stopped + +# Uncomment the following lines for API rate limit and email sending +# fittrackee-redis: +# image: "redis:7.4" +# container_name: fittrackee-redis +# hostname: redis +# volumes: +# - ${REDIS_DIR:-./data/redis}:/data +# healthcheck: +# test: ['CMD', 'redis-cli', 'ping'] +# networks: +# - internal_network +# restart: unless-stopped + +networks: + external_network: + internal_network: + internal: true \ No newline at end of file diff --git a/docker-entrypoint-dev.sh b/docker-entrypoint-dev.sh new file mode 100644 index 000000000..69856551c --- /dev/null +++ b/docker-entrypoint-dev.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +# Init database +echo "Initializing database..." +ftcli db upgrade || { echo "Failed to upgrade database!"; exit 1; } + +# Run workers +echo "Starting workers..." +flask worker --processes="${WORKERS_PROCESSES:-1}" >> data/logs/dramatiq.log 2>&1 & + +# Wait for workers to start +sleep 3 + +# Run app +echo "Starting app..." +exec flask run --with-threads --host=0.0.0.0 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 3fe02e0e2..3ba387392 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,28 +1,10 @@ #!/bin/bash set -e -# Change to the application directory -cd /usr/src/app || exit 1 - -# Check if the .env file exists and source it -if [[ -f .env ]]; then - source .env -else - echo ".env file not found!" - exit 1 -fi - -# Init database -echo "Initializing database..." +# Upgrade database +echo "Upgrading database..." ftcli db upgrade || { echo "Failed to upgrade database!"; exit 1; } -# Run workers -echo "Initializing workers..." -flask worker --processes="${WORKERS_PROCESSES:-1}" >> dramatiq.log 2>&1 & - -# Wait for workers to start -sleep 3 - -# Run app -echo "Initializing app..." -exec flask run --with-threads --host=0.0.0.0 +# Run app w/ gunicorn +echo "Running app..." +exec gunicorn -b 0.0.0.0:5000 "fittrackee:create_app()" --error-logfile /usr/src/app/logs/gunicorn.log diff --git a/docker/lint-python.sh b/docker/lint-python.sh deleted file mode 100755 index cbf9435e1..000000000 --- a/docker/lint-python.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e -cd /usr/src/app - -source .env - -mypy fittrackee -ruff check fittrackee e2e \ No newline at end of file diff --git a/docker/set-admin.sh b/docker/set-admin.sh deleted file mode 100755 index 84eeb7c08..000000000 --- a/docker/set-admin.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e -cd /usr/src/app - -source .env - -ftcli users update $1 --set-admin true diff --git a/docker/shell.sh b/docker/shell.sh deleted file mode 100755 index 55fd7607d..000000000 --- a/docker/shell.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e -cd /usr/src/app - -source .env - -/bin/bash \ No newline at end of file diff --git a/docker/test-e2e.sh b/docker/test-e2e.sh index 97e26e0cd..09b549cfa 100755 --- a/docker/test-e2e.sh +++ b/docker/test-e2e.sh @@ -1,8 +1,5 @@ #!/bin/bash set -e -cd /usr/src/app - -source .env export TEST_APP_URL=http://$(hostname --ip-address):5000 pytest e2e --driver Remote --capability browserName firefox --selenium-host selenium --selenium-port 4444 $* \ No newline at end of file diff --git a/docker/test-python.sh b/docker/test-python.sh deleted file mode 100755 index 8058b5041..000000000 --- a/docker/test-python.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e -cd /usr/src/app - -source .env - -pytest fittrackee $* \ No newline at end of file diff --git a/docsrc/source/installation.rst b/docsrc/source/installation.rst index 33a2cc03e..18458ac15 100644 --- a/docsrc/source/installation.rst +++ b/docsrc/source/installation.rst @@ -260,6 +260,48 @@ deployment method. .. versionchanged:: 0.7.26 ⚠️ replaces ``VUE_APP_API_URL`` + **FitTrackee** API URL, only needed in dev environment. + +Docker +^^^^^^ + +.. versionadded:: 0.8.13 + +Environment variables for ``docker-compose.yml`` + +.. envvar:: DATABASE_DIR + + Host directory for PostgreSQL data volume + + +.. envvar:: POSTGRES_USER + + User for PostgreSQL database + + +.. envvar:: POSTGRES_PASSWORD + + Password for PostgreSQL user + + +.. envvar:: POSTGRES_DB + + Database name for FitTrackee application + + +.. envvar:: REDIS_DIR + + Host directory for redis data volume + + +.. envvar:: LOG_DIR + + Host directory for logs volume + + +.. envvar:: UPLOAD_DIR + + Host directory for uploaded files volume Emails @@ -834,15 +876,50 @@ Examples: Docker ~~~~~~ -Installation -^^^^^^^^^^^^ - .. versionadded:: 0.4.4 +.. versionchanged:: 0.5.0 add client application for development +.. versionchanged:: 0.8.13 add docker image for production + -For **evaluation** purposes, docker files are available, installing **FitTrackee** from **sources**. +Production +^^^^^^^^^^ + +Images are available on `DockerHub `_ or `Github registry `_. + +.. note:: + + Images are available for ``linux/amd64`` and ``linux/arm64`` platforms. Only ``linux/amd64`` image has been tested. + +- create a ``docker-compose.yml`` file as needed (see the example in the repository): + + - the minimal set up requires at least the database and the web application + - to activate the rate limit, redis is required + - to send e-mails, redis and workers are required and a valid ``EMAIL_URL`` variable must be set in ``.env`` + +.. note:: + The same image is used by the web application and workers. + +- create ``.env`` from example (``.env.docker.example``) and update it (see `Environment variables `__). + +- to start the application: + +.. code:: bash + + $ docker compose up -d .. warning:: - Docker files are not suitable for production installation. + + Migrations are executed at startup. Please backup data before updating FitTrackee image version. + +- to run a CLI command, for instance to give admin rights: + +.. code:: bash + + $ docker compose exec fittrackee ftcli users update --set-admin true + + +Development +^^^^^^^^^^^ - To install and run **FitTrackee**: @@ -850,7 +927,6 @@ For **evaluation** purposes, docker files are available, installing **FitTrackee $ git clone https://github.com/SamR1/FitTrackee.git $ cd FitTrackee - $ cp .env.docker .env $ make docker-run - Open http://localhost:5000 and register. @@ -878,12 +954,6 @@ Open http://localhost:8025 to access `MailHog interface