Python container app med hot-reload

· 565 ord · 3 min å lese

Se endringene momentant i din Python applikasjon. I denne artikkelen tar jeg for meg bruk og oppsett av watchfiles i en docker-container!

Fordelen med å skrive et kommandotolket (interpreted) språk som Python er at man slipper å kompilere koden mellom hver endring. Dette gjør at man i praksis kan se endringene i applikasjonen man utvikler med en gang. De fleste rammeverk har som regel også støtte for denne typen hot-reload fra før — hurra! 🎉

Likevel, det finnes unntak som Celery hvor dette må håndteres selv. Og hva med egne applikasjoner, rammeverk og skript? Jo, her står vi på egne ben.

Heldigvis finnes det allerede løsninger for oppdagelse av filendringer og restart av applikasjon i Python, nemlig watchfiles. Denne pakken bruker i bunn rust som gjør at vi slipper kollisjon av tråder i Python. Pakken vedlikeholdes av @samuelcolvin, mannen bak godbiter som pydantic og arq.

Implementasjon 🔗

watchfiles har et minimalt grensesnitt som tillater både lytting etter endringer i kode og CLI for kjøring og restart av kommandoer/skript. Ettersom formålet er å kunne restarte en applikasjon inni en docker-container, vil CLI-varianten være egnet løsning her.


All kode er forresten tilgjengelig på repoet endrekrohn/hot-reload-python-app 😎

Klargjøring av docker-compose.yml 🔗

Vi tar utgangspunkt i denne docker-compose-filen med en enkelt applikasjon:

services:
  application:
    restart: always
    init: true
    build:
      context: ./application
      args:
        INSTALL_DEV: ${INSTALL_DEV-false}

Videre vet jeg at jeg kun ønsker hot-reloading i utviklingsmiljøet av applikasjonen. Jeg oppretter derfor følgende docker-compose.override.yml.

services:
  application:
    command: ./start-reload.sh
    volumes:
      - ./application/code:/application/code
    build:
      args:
        INSTALL_DEV: ${INSTALL_DEV-true}

Her er det tre ting å legge merke til:

  1. Kommandoen er satt til et spesielt start-reload.sh-skript som jeg senere skal definere.
  2. Mitt lokale volum er mappet til volum i container slik at endringer gjenspeiles.
  3. Flagget INSTALL_DEV er satt slik at utvikleravhengigheter innstalleres.

Installasjon av watchfiles i container 🔗

I og med at jeg bruker poetry som pakkesjef kan jeg innstallere watchfiles som utvikleravhengighet med følgende kommando:

poetry add -D watchfiles

Med flagget INSTALL_DEV satt til true vil da min dockerfile innstallere denne pakken.

# Allow installing dev dependencies to run tests
ARG INSTALL_DEV=false
RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry export -f requirements.txt --without-hashes -o requirements.txt --dev ; else poetry export -f requirements.txt --without-hashes -o requirements.txt ; fi"

Jeg anbefaler å ta en titt på hvordan denne dockerfilen er satt opp for å skille avhengigheter. Kanskje kunne det også vært aktuelt med et eget innlegg om denne filen 🤔

Opprette start-reload.sh-skript 🔗

Deretter må jeg definere skriptet som kjøres i docker-compose.override.yml. Dette skriptet bruker watchfiles sitt CLI-grensesnitt til å definere inngangsskriptet, samt hvilke(n) mapper den skal lytte til.

#! /usr/bin/env bash

watchfiles 'python ./code/main.py' ./code

Demonstrasjon 🔗

Til slutt kan vi kjøre opp applikasjonen med docker-compose, gjøre noen endringer og se resultatet med en gang.

hot-reload-python-demo

Til slutt 🔗

Avslutningsvis vil jeg påpeke en obs å være klar over.

Dersom du skulle bruke multiprocessing-pakken i pythons standardbibliotek er det viktig å avslutte prosessene.

Ved prosesser som bruker uviss tid bør du derfor bruke en event som håndterer eventuelle while-loops du måtte ha.

from multiprocessing.synchronize import Event

def some_task(shutdown: Event) -> None:
    while not shutdown.is_set():
        do_work()

Om du ikke avslutter disse gracefully vil det etter en viss tid sendes et signal om å drepe hele prosessen 💀. Tiden det tar før watchfiles sender signal kan justeres med flaggene: --sigint-timeout og --sigkill-timeout.

Dette gjelder for øvrig alle koblinger du måtte ha åpen, eksempelvis til en database eller fil.

Docker sender også SIGINT på samme måte som watchfiles. Bruk av hot-reloading og watchfiles kan dermed forebygge eventuelle smutthull du skulle ha i din docker-applikasjon.