Introduction
Deployment is an issue I’m having with “is it Easter”. I was deploying by getting a zip of the project from GitHub, extracting and copying the folder to the server. I’d then have to copy files into the app directory as well as create the instance folder for the config file. This is a very cumbersome process and error prone.
While I do have a Dockerfile and can build a docker image, I’m not using
Docker for my deployments. It would make things a bit easier but I still have
the issue of needing to create a mount in the application directory within the
container for images and the config file.
While Docker is an excellent way distribute applications, Python has it’s own package format for distributing applications and modules. Known as wheels. Packing “is it Easter” as a wheel would be handy.
Necessary Changes
Packaging as a wheel wasn’t possible right out of the box with how I designed the app and there are a few issues I need to resolve.
- Directory layout needs to be changed.
- A build system that can build a wheel is needed.
- The way I’m handing configure files with Flask requires the main config
to be located one level up from the module. This won’t work because it
would require the file to be installed in
site-packages. - I don’t want to install config and image files into the installation directory.
Directory Restructuring
When building a wheel, the application is installed in site packages and uses
the same name as the directory. I was using app as the directory name
so installation would install the application module as site-packages/app.
I had to change isiteaster/app to isiteaster/isiteaster in order
to have the wheel install properly. This is an easy change and a simple
rename.
While I renamed the app directory, I’m using the old name in some places in
this post to make it easier to differentiate the top level from the module directory.
PDM Build System
I decided to use PDM as the build system because it’s easy to setup and
understand. This required me to start using a pyproject.toml file; it’s a
really good thing. It allows me to add metadata about the application.
I also moved form using requirements.txt for dependency management
and I’m using the pyproject.toml file.
Here is an expert of the file.
[build-system]
# pybabel on macOS requires setuptools to be installed in order to work properly
requires = ["pdm-backend", "setuptools", "Babel"]
build-backend = "pdm.backend"
[project]
name = "isiteaster"
...
license = { text = "AGPL-3.0-only"}
keywords = [ "easter", "web", "flask" ]
requires-python = ">=3.11"
dependencies = [
"Flask >= 3",
...
]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Flask",
...
]
[project.urls]
...
[tool.pdm.build]
source-includes = [ "**/*.po", "babel.cfg" ]
excludes = [ "**/messages.pot", "**/.DS_Store", "**/.*.swp", "**/.gitignore" ]
[tool.pdm.scripts]
pre_build = "pybabel compile -d isiteaster/translations"
Another nice thing about using PDM is, it lets me specify actions to take
prior to building. I have pre_build hook that will automatically compile
.po translations files into .mo files.
Running pdm build will generate a source package as a tar.gz and a .whl
file that can be installed with pip.
Configuration Files
Configuration file handling was the hardest problem to solve. The main pattern I see for setting up configuration files in Flask is:
app = Flask(__name__, instance_relative_config=True)
app.config.from_object('config')
app.config.from_pyfile('config.py', silent=True)
from_object for default config
The problem is from_object will load a file called config.py from one level
above the module. This isn’t going to work with a wheel because one level up is
the site-packages directory and a generic config.py file should not be
there.
One way I’ve seen this is nesting the module in another module like:
top/wrapper/app. Where all three directories would have the same name.
Wrapper would be a module and have config.py and app would have the app
itself (also a module). This creates a cumbersome layout and isn’t really
solving the problem. Adding layers of indirection isn’t a valid solution.
I’m only using the config.py file to set default settings and it isn’t a file
that should be edited. I’m relying on the instance config file for user
specified settings. Instead of relying on from_object to find the config file
I’m specifying the default file myself.
- app.config.from_object('config')
+ app.config.from_pyfile(os.path.join(app.root_path, 'config_default.py'))
Using from_pyfile with an absolute path, it will load that specific file instead
of trying to load from a predetermined location outside of the module directory.
from_pyfile for instance config
Flask’s instance config files are very handy and when using from_pyfile with
a relative location it will look for an instance directory outside of the module
root to load the config file.
The instance location will vary based on whether the application is installed as a wheel.
Running locally the instance directory is one level above the module.
For example, isiteaster/app/ the instance directory will be located
in isiteaster beside app.
When installed as a wheel, the instance directory is located at
var/<project_name>-instance. var is relative to the installation
path of Python’s site-packages. For example, /var, /usr/var,
or /usr/local/var.
You can find the instance directory using app.instance_path or
app.auto_find_instance_path().
When using app.config.from_pyfile('config.py', silent=True) the
application will attempt to load <instance_path>/config.py.
I’m setting silent=True to ignore failures if the file is not found.
Since it’s an override for default settings (stacked on previously loaded
config files), it doesn’t matter if it’s not present.
Environment variable
While the instance directory can be used, I also added support for loading the configuration from an environment variable. This is a convince and makes it easier to specify a configuration file as a Docker mount point.
app.config.from_envvar('ISITEASTER_CONF', silent=True)
The application will check if the variable exists and try to load the
referenced file. Once again I’m using silent=True in order to ignore errors
if the file is missing since it’s not required.
External File Handling
Right now I have to put images directly into the application when it’s
installed. Which is not a good idea and it’s possible to wipe out the files
when upgrading. I added a new config variable IMAGE_DIR which allows
specifying a location on disk where images are located.
Now I can have the images in a location completely separate from the application. There won’t be issues with non-package manager managed files and I won’t have to worry about them getting deleted during an upgrade.
Docker
I have a working Docker setup for packaging “is it Easter” but with the changes to allow building as a wheel, I can make the process much cleaner. I’m able to use a split Docker build where I copy the project into the image, install the dependencies and build with PDM.
Then I can take the .whl file and install it in a release image. Additionally,
I can use the config changes to set an environment variable in the image
to load the config located at /data/isiteaster.conf. In the config file
I have the IMAGE_DIR set to /data/images. Allowing for a consistent and
unchanging location for the configuration and images to be loaded into the container.
Conclusion
I’m happy to have learned more about packaging in general and I’m especially happy that “is it Easter” has a much cleaner installation and deployment strategy.
This project was a challenge because the majority of the information I found online, even the official documentation, wasn’t clear about what’s needed to package as a wheel. The little bit of information I found just said it’s a good idea but all the example projects I looked at don’t follow the pattern I’ve outlined and won’t work as a wheel.
What I learned about packaging Flask apps can be summarized as follows. Don’t use from_object
for loading configuration files. Make the configuration loadable via an environment variable.
Make locations configurable via a setting. Make the module directory the same name
as the project name. Finally, prefer Docker as the deployment method.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.