Cannot run entrypoint script in Docker with Flask and Flask Migrate, even though it works in Terminal

1.8k Views Asked by At

I have a Flask API that connects to an Azure SQL database, deployed on Azure App Service in a Docker Image.

It works fine but I am trying to keep consistency between my development, staging and production environments using Alembic/Flask-Migrate to apply database upgrades.

I saw on Miguel Grinberg's Docker Deployment Tutorial, that this can be achieved by adding the flask db upgrade command to a boot.sh script, like so:

#!/bin/sh
flask db upgrade
exec gunicorn -w 4 -b :5000 --access-logfile - --error-logfile - app:app

My problem is that, when running the boot.sh script, I receive the error:

Usage: flask db [OPTIONS] COMMAND [ARGS]...
Try 'flask db --help' for help.

'.ror: No such command 'upgrade

Which indicates the script cannot find the Flask-Migrate library. This actually happens if I try other site-packages, such as just trying to run flask commands.

The weird thing is:

  • gunicorn works just fine
  • The API works just fine
  • I can run flask db upgrade with no problem if I fire up the container and open a terminal session with docker exec -i -t api /bin/sh

Obviously, there's a problem with my Dockerfile. I would massively appreciate any help here as I'm relatively new to Docker and Linux so I'm sure I'm missing something obvious:

EDIT: It also works just fine if I add the following line to my Dockerfile, just before the entrypoint CMD:

RUN flask db upgrade

Dockerfile

FROM python:3.8-alpine

# Dependencies for pyodbc on Linux
RUN apk update
RUN apk add curl sudo build-base unixodbc-dev unixodbc freetds-dev
RUN apk add gcc musl-dev libffi-dev openssl-dev
RUN apk add --no-cache tzdata
RUN rm -rf /var/cache/apk/*
RUN curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.2.2-1_amd64.apk
RUN sudo sudo apk add --allow-untrusted msodbcsql17_17.5.2.2-1_amd64.apk

RUN mkdir /code
WORKDIR /code

COPY requirements.txt requirements.txt
RUN python -m pip install --default-timeout=100 -r requirements.txt
RUN python -m pip install gunicorn
ADD . /code/

COPY boot.sh /usr/local/bin/    
RUN chmod u+x /usr/local/bin/boot.sh
EXPOSE 5000
ENTRYPOINT ["sh", "boot.sh"]
2

There are 2 best solutions below

0
On BEST ANSWER

I ended up making some major changes to my Dockerfile and boot.sh script. I'll share these as best I can below:

Problem 1: Entrypoint script cannot access directories

My main issue was that I had an inconsistent folder structure in my directory. There were 2 boot.sh scripts and the one being run on entrypoint either had the wrong permissions or was in the wrong place to find my site packages.

I simplified the copying of files from my local machine to the Docker image like so:

RUN mkdir /code
WORKDIR /code

COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN venv/bin/pip install --default-timeout=100 -r requirements.txt
RUN venv/bin/pip install gunicorn

COPY app app
COPY migrations migrations
COPY api.py config.py boot.sh ./
RUN chmod u+x boot.sh

EXPOSE 5000
ENTRYPOINT ["./boot.sh"]

The changes involved:

  • Setting up a virtualenv and installing all site packages in there
  • Making sure the config.py, boot.sh, and api.py files were in the root directory of the application folder (./)
  • Changing the entrypoint command from ["bin/sh", "boot.sh"] to just ["./boot.sh"]
  • Moving migrations files into the relevant folder for the upgrade script

I was then able to activate the virtual environment in the entrypoint file, and run the flask upgrade commands (NB: I had a problem with line endings being CRLF instead of LF in boot.sh, so make sure to change it if on Windows):

#!/bin/bash
source venv/bin/activate
flask db upgrade
exec gunicorn -w 4 -b :5000 --access-logfile - --error-logfile - api:app

Problem 2: Alpine Linux Too Slow

My other issue was that my image was taking forever to build (upwards of 45 mins) on Alpine Linux. Turns out this is a pretty well-established issue when using some of the libraries in my API (Pandas, Numpy).

I switched to a Debian build so that I could makes changes more quickly to my Docker image.

Including the installation of pyodbc to connect to Azure SQL Server, the first half of my Dockerfile now looks like:

FROM python:3.8-slim-buster

RUN apt-get update
RUN apt-get install -y apt-utils curl sudo gcc g++ gnupg2

RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get install -y libffi-dev libgssapi-krb5-2 unixodbc-dev unixodbc freetds-dev 
RUN sudo apt-get update
RUN sudo ACCEPT_EULA=Y apt-get install msodbcsql17
RUN apt-get clean -y

Where the curl commands and below come from the official MS docs on installing pyodbc on Debian

Full dockerfile:

FROM python:3.8-slim-buster

RUN apt-get update
RUN apt-get install -y apt-utils curl sudo gcc g++ gnupg2
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get install -y libffi-dev libgssapi-krb5-2 unixodbc-dev unixodbc freetds-dev 
RUN sudo apt-get update
RUN sudo ACCEPT_EULA=Y apt-get install msodbcsql17
RUN apt-get clean -y

RUN mkdir /code
WORKDIR /code

COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN venv/bin/pip install --default-timeout=100 -r requirements.txt
RUN venv/bin/pip install gunicorn

COPY app app
COPY migrations migrations
COPY api.py config.py boot.sh ./
RUN chmod u+x boot.sh

EXPOSE 5000
ENTRYPOINT ["./boot.sh"]
3
On

I think this is the key information.

Which indicates the script cannot find the Flask-Migrate library. This actually happens if I try other site-packages, such as just trying to run flask commands.

To me this may indicate that the problem is not specific to Flask-Migrate but to all packages - as you write. This may mean on of following two.

First, it can mean that the packages are not correctly installed. However, this is unlikely as you write that it works when you manually start the container.

Second, something is wrong with how you execute your boot.sh script. For example, try changing

ENTRYPOINT ["sh", "boot.sh"]

to

ENTRYPOINT ["/bin/sh", "boot.sh"]

HTH!