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.