Docker deployment¶
What you will build¶
In this tutorial you will take a development configuration and transform it into a production-ready Zope configuration using environment variables. This is the standard pattern for containerized (Docker / Kubernetes) deployments where secrets and runtime settings must not be baked into images.
Prerequisites¶
A working
instance.yamlfrom the Your first Zope instance tutorial.The
transform_from_environment.pyhelper script (found in thehelpers/directory of the cookiecutter template).Python 3.10+
Step 1: start with a development config¶
Use the same instance.yaml you created earlier:
default_context:
initial_user_name: 'admin'
initial_user_password: 'admin'
zcml_package_includes: my.awesome.addon
db_storage: direct
This is your base configuration. Environment variables will override individual values at build time.
Step 2: set production environment variables¶
The transform script recognizes variables prefixed with INSTANCE_. Each
variable name maps to a key in default_context – the prefix is stripped
and the remainder becomes the key.
Export the following variables in your shell:
export INSTANCE_wsgi_fast_listen=
export INSTANCE_wsgi_listen=127.0.0.1:8080
export INSTANCE_initial_user_password=
export INSTANCE_debug_mode=false
export INSTANCE_verbose_security=false
export INSTANCE_db_storage=relstorage
export INSTANCE_db_blob_mode=cache
export INSTANCE_db_relstorage_keep_history=false
export INSTANCE_db_relstorage=postgresql
export INSTANCE_db_relstorage_postgresql_dsn="host='db' dbname='plone' user='plone' password='verysecret'"
Notable choices:
Empty values (
INSTANCE_wsgi_fast_listen=,INSTANCE_initial_user_password=) clear the key – the fast-listen port is disabled and no initial user is written.db_storage=relstorageswitches from local FileStorage to RelStorage.db_relstorage_postgresql_dsnpoints at adbhost – the typical name for a database service in Docker Compose.
Step 3: run the transform¶
python transform_from_environment.py
The script reads instance.yaml, merges the INSTANCE_* variables on top,
and writes instance-from-environment.yaml.
Step 4: examine the output¶
The effective context will look like:
default_context:
initial_user_name: 'admin'
initial_user_password: ''
zcml_package_includes: my.awesome.addon
db_storage: relstorage
db_blob_mode: cache
db_relstorage_keep_history: false
db_relstorage: postgresql
db_relstorage_postgresql_dsn: "host='db' dbname='plone' user='plone' password='verysecret'"
wsgi_fast_listen: ''
wsgi_listen: '127.0.0.1:8080'
debug_mode: false
verbose_security: false
Feed this into cookiecutter to generate the production instance directory:
cookiecutter -f --no-input \
--config-file instance-from-environment.yaml \
gh:plone/cookiecutter-zope-instance
Step 5: use in a Dockerfile¶
The transform must run at container startup, not at image build time,
because INSTANCE_* environment variables (database credentials, hostnames)
are only available at runtime.
The image bakes in a pinned version of the cookiecutter template (downloaded at build time) plus the transform helper. The entrypoint generates the Zope configuration from environment variables on every container start – no network access required at runtime.
Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Pin the cookiecutter-zope-instance version
ENV COOKIECUTTER_ZOPE_INSTANCE_VERSION=2.3.0
# Install cookiecutter and your application
RUN pip install cookiecutter
# Download pinned template + transform helper at build time (no network at runtime)
RUN wget -O /app/cookiecutter-zope-instance.zip \
https://github.com/plone/cookiecutter-zope-instance/archive/refs/tags/${COOKIECUTTER_ZOPE_INSTANCE_VERSION}.zip \
&& wget -O /app/transform_from_environment.py \
https://raw.githubusercontent.com/plone/cookiecutter-zope-instance/${COOKIECUTTER_ZOPE_INSTANCE_VERSION}/helpers/transform_from_environment.py
# Copy base config and entrypoint
COPY instance.yaml /app/instance.yaml
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# Sensible defaults (overridden at runtime via docker run -e / Compose)
ENV INSTANCE_wsgi_listen=0.0.0.0:8080
ENV INSTANCE_debug_mode=false
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["start"]
entrypoint.sh
#!/bin/bash
set -e
# Generate instance.yaml from INSTANCE_* environment variables
python /app/transform_from_environment.py -o /app/instance-from-environment.yaml
# Generate Zope instance configuration from local template (no network needed)
cookiecutter -f --no-input \
--config-file /app/instance-from-environment.yaml \
/app/cookiecutter-zope-instance.zip
if [[ "$1" == "start" ]]; then
exec runwsgi instance/etc/zope.ini
else
exec "$@"
fi
The template and helper are downloaded and pinned at build time, and the entrypoint runs only against local files, so no network access is needed at startup. For the reasoning behind generating at startup and pinning the version, see The configuration workflow.
Now start the container with runtime environment variables:
docker run -e INSTANCE_db_storage=relstorage \
-e INSTANCE_db_relstorage=postgresql \
-e INSTANCE_db_relstorage_postgresql_dsn="host='db' dbname='plone' user='plone' password='secret'" \
-p 8080:8080 my-zope-app
Or via Docker Compose:
services:
zope:
build: .
environment:
INSTANCE_db_storage: relstorage
INSTANCE_db_relstorage: postgresql
INSTANCE_db_relstorage_postgresql_dsn: "host='db' dbname='plone' user='plone' password='secret'"
ports:
- "8080:8080"
Set dictionary values¶
Some template variables are dictionaries.
Use the _DICT_ infix to set a single entry inside one:
export INSTANCE_environment_DICT_TZ=Europe/Berlin
See The configuration workflow for how the _DICT_ infix
works.
What you learned¶
The
transform_from_environment.pyscript bridges environment variables and the cookiecutter YAML configuration.Every
INSTANCE_<key>variable maps to adefault_contextentry.Empty values clear a key – useful for disabling features in production.
The
_DICT_infix lets you set individual entries inside dict-typed variables.The transform and cookiecutter must run in an entrypoint, not at build time, so that runtime environment variables (secrets, DSNs) are available.