Files
romm/docker/init_scripts/init
Michael Manganiello 0daa708a05 misc: Simplify backend environment configuration in Docker image
This change moves the virtualenv creation in the `Dockerfile` to a
separate stage, to simplify isolating the process and reduce the need
for uninstalling build dependencies.

The approach is similar to the one explained in [1]. It relies on
building a virtualenv folder, and copying it in the final stage.
Changing the `PATH` environment variable makes the virtualenv usable by
default, without affecting the default Python installation.

Also, added Dockerfile arguments for Alpine, nginx, and Python versions,
as some of them are reused, and also simplifies testing new versions.

An extra side effect is that the image size for the final stage is
reduced from 315MB to 262MB.

[1] https://scribe.rip/@albertazzir/blazing-fast-python-docker-builds-with-poetry-a78a66f5aed0
2024-06-22 18:19:16 -03:00

168 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -o errexit # treat errors as fatal
set -o nounset # treat unset variables as an error
set -o pipefail # treat errors in pipes as fatal
shopt -s inherit_errexit # inherit errexit
# make it possible to disable the inotify watcher process
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE="${ENABLE_RESCAN_ON_FILESYSTEM_CHANGE:="true"}"
# Set INIT_DEBUG to false if not set by docker env
# (this env var is currently undocumented and usually just needed for development purposes)
INIT_DEBUG="${INIT_DEBUG:="false"}"
# print debug log output if enabled
debug_log() {
if [[ ${INIT_DEBUG} == "true" ]]; then
echo "DEBUG: [init][$(date +"%Y-%m-%d %T")]" "${@}" || true
fi
}
# print debug log output if enabled
info_log() {
echo "INFO: [init][$(date +"%Y-%m-%d %T")]" "${@}" || true
}
# print error log output if enabled
error_log() {
echo "ERROR: [init][$(date +"%Y-%m-%d %T")]" "${@}" || true
exit 1
}
wait_for_gunicorn_socket() {
info_log "waiting for gunicorn socket file..."
while [[ ! -S /tmp/gunicorn.sock ]]; do
sleep 1
done
info_log "gunicorn socket file found"
}
# function that runs or main process and creates a corresponding PID file,
start_bin_gunicorn() {
# cleanup potentially leftover socket
rm /tmp/gunicorn.sock -f
# commands to start our main application and store its PID to check for crashes
info_log "starting gunicorn"
gunicorn \
--access-logfile - \
--error-logfile - \
--worker-class uvicorn.workers.UvicornWorker \
--bind=0.0.0.0:5000 \
--bind=unix:/tmp/gunicorn.sock \
--pid=/tmp/gunicorn.pid \
--workers "${GUNICORN_WORKERS:=2}" \
main:app &
}
# Commands to start nginx (handling PID creation internally)
start_bin_nginx() {
wait_for_gunicorn_socket
info_log "starting nginx"
if [[ ${EUID} -ne 0 ]]; then
nginx
else
# if container runs as root, drop permissions
nginx -g 'user romm;'
fi
}
start_bin_redis-server() {
info_log "starting redis-server"
# Check if /usr/local/etc/redis/redis.conf exists and use it if so
if [[ -f /usr/local/etc/redis/redis.conf ]]; then
redis-server /usr/local/etc/redis/redis.conf &
else
redis-server --dir /redis-data &
fi
REDIS_PID=$!
echo "${REDIS_PID}" >/tmp/redis-server.pid
}
# function that runs our independent python scripts and creates corresponding PID files,
start_python() {
SCRIPT="${1}"
info_log "starting ${SCRIPT}.py"
python3 "${SCRIPT}.py" &
WATCHER_PID=$!
echo "${WATCHER_PID}" >"/tmp/${SCRIPT}.pid"
}
watchdog_process_pid() {
TYPE=$1
PROCESS=$2
if [[ -f "/tmp/${PROCESS}.pid" ]]; then
# check if the pid we last wrote to our state file is actually active
PID=$(cat "/tmp/${PROCESS}.pid") || true
if [[ -d "/proc/${PID}" ]]; then
debug_log "${PROCESS} still running, no need to start"
else
if [[ ${TYPE} == "bin" ]]; then
start_bin_"${PROCESS}"
elif [[ ${TYPE} == "python" ]]; then
start_python "${PROCESS}"
fi
fi
else
if [[ ${TYPE} == "bin" ]]; then
start_bin_"${PROCESS}"
elif [[ ${TYPE} == "python" ]]; then
start_python "${PROCESS}"
fi
fi
}
# switch to backend directory
cd /backend || { error_log "/backend directory doesn't seem to exist"; }
info_log "Starting up, please wait..."
# clear any leftover PID files
rm /tmp/*.pid -f
# function definition done, lets start our main loop
while true; do
# check for died processes every 5 seconds
sleep 5
# Start redis server if we dont have a corresponding PID file
# and REDIS_HOST is not set (which would mean we're using an external redis)
if [[ -z ${REDIS_HOST:=""} ]]; then
watchdog_process_pid bin redis-server
fi
# Run needed database migrations on startup,
# but only if it was not successful since the last full docker container start
if [[ ${ALEMBIC_SUCCESS:="false"} == "false" ]]; then
if alembic upgrade head; then
debug_log "database schema migrations suceeded"
ALEMBIC_SUCCESS="true"
else
error_log "Something went horribly wrong with our database"
fi
else
debug_log "database schema already upgraded during current container lifecycle"
fi
# Start gunicorn if we dont have a corresponding PID file
watchdog_process_pid bin gunicorn
# Start nginx if we dont have a corresponding PID file
watchdog_process_pid bin nginx
# only start the watcher.py if we actually want to use the rescan on fs change feature
if [[ ${ENABLE_RESCAN_ON_FILESYSTEM_CHANGE} == "true" ]]; then
# Start watcher if we dont have a corresponding PID file
watchdog_process_pid python watcher
fi
# Start background worker processes
debug_log "Starting worker and scheduler"
# Start worker if we dont have a corresponding PID file
watchdog_process_pid python worker
# Start scheduler if we dont have a corresponding PID file
watchdog_process_pid python scheduler
done