In a previous blog we showed you how to strangle old code using Python decorators. This 5 minute blog post shows you how to run a Python Flask server in a readonly Docker container. The steps are implemented in the public tdd repo.
docker-compose.yml
The yaml to run a Flask server inside a read-only container is simple:
services:
xy_demo:
...
read_only: true
tmpfs: [ /tmp ]
Python cache files
When Python runs it creates .pyc cache files close to the cached files. It can’t do this in a read-only container. You can set the PYTHONPYCACHEPREFIX
environment variable in the server’s Dockerfile to a directory it can write to, such as a subdir of /tmp.
ENV PYTHONPYCACHEPREFIX=/tmp/py_caches
Typical asset bundling won’t work
In a typical server startup, asset files are bundled together. In the example below, all the separate CSS files in the static/css/
directory get bundled into a new CSS file called bundle.css
in the same directory:
from flash import Flask
from flask_assets import Environment, Bundle
from pathlib import Path
def app():
server = Flask(__name__)
assets = Environment(server)
init_css(server, assets)
...
def init_css(server, assets):
css_files = asset_file_paths(server, "css")
css = Bundle(*css_files, filters='cssmin', output='bundle.css')
server.register('css', css)
def asset_file_paths(server, dir_name):
static_path = Path(f'{server.root_path}/static/{dir_name}')
return [f'{static_path}/{file.name}' for file in static_path.iterdir()]
This won’t work in a read-only container because Python will not be able to create new files inside the ../static/css/
directory.
Static asset pre-bundling
To overcome this problem, Kosli pre-bundles the SCSS and JS files in a separate step which runs before building the server image. The bash commands to do this are:
GIT_COMMIT_SHA=”$(git rev-parse HEAD)”
docker run --rm \
--volume "${HOST_ROOT_DIR}/package.json:/app/package.json:ro" \
--volume "${HOST_ROOT_DIR}/source/static/scss:/app/scss:rw" \
--volume "${HOST_ROOT_DIR}/source/static/js:/app/js:rw" \
--env GIT_COMMIT_SHA \
assets-builder:v1 \
bash -c "npm run build"
When these commands complete successfully:
- The SCSS files are bundled into a file called
bundle.${GIT_COMMIT_SHA}.css
- The JS files are bundled into a file called
bundle.${GIT_COMMIT_SHA}.js
The package.json file is as follows:
{
"name": "kosli",
"version": "1.0.0",
"description": "kosli css",
"main": "index.js",
"directories": { "doc": "docs" },
"scripts": {
"remove:assets": "rm -f scss/bundle.*.css js/bundle.*.js",
"compile:css": "sass --no-source-map ... --style expanded",
"prefix:css": "postcss ... -o scss/bundle.prefixed.css",
"compress:css": "sass --no-source-map ... --style compressed",
"cleanup:css": "rm scss/bundle.prefixed.css scss/bundle.css",
"rename:css": "mv ... scss/bundle.${GIT_COMMIT_SHA}.css",
"bundle:js": "bundle-js ... js/bundle.${GIT_COMMIT_SHA}.js",
"build": "npm-run-all remove:assets ..."
},
...
"devDependencies": {
"autoprefixer": "^10.4.12",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.18",
"postcss-cli": "^10.0.0",
"sass": "^1.55.0",
"bundle-js": "^1.0.3"
}
}
The assets-builder:v1 docker image is created from the file Dockerfile.assets
FROM node
WORKDIR /app
RUN npm i sass postcss postcss-cli autoprefixer npm-run-all bundle-js --save-dev
using the bash command:
docker build --tag assets-builder:v1 -f Dockerfile.assets .
The server source has two functions to return the path to the pre-bundled files:
from flask import url_for
import os
def bundle_css():
return url_for(‘static’, filename=f”scss/bundle.{git_commit_sha()}.css”)
def bundle_js():
return url_for(‘static’, filename=f”js/bundle.{git_commit_sha()}.js”)
def git_commit_sha():
return os.environ.get("GIT_COMMIT_SHA")
These functions are called in the html head section:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
...
<link rel="stylesheet" href="{{ bundle_css }}" />
<script src="{{ bundle_js }}"></script>
</head>
<body>
...
</body>
</html>
Congratulations! You now know how to run a Python Flask server in a readonly Docker container.