Docs / How-to guides / Deploy
How-to guide For operators

Deploy

One small Python service behind nginx. No container, no build step, no queue.

See the deployment view for what runs where on the box, and what it can reach outside it.

Before you start

  • A Linux server with Python 3, nginx and systemd, and sudo.
  • The three files in deploy/: the systemd unit (web, loopback port 8300), the nginx location /dataproductfactory block, and deploy.sh — install, test, restart, confirm it is up; a failing test leaves the old version serving.

Steps

First install

1. Create the service account's directory and put a checkout of the repository in it, then install the dependencies as web:

sudo mkdir -p /srv/dataproductfactory && sudo chown web:web /srv/dataproductfactory
# copy the repository there, then, as web:
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

2. Write the environment file — root-owned and mode 600, because a unit file under /etc/systemd is world-readable and a secret inside it would not be a secret:

sudo install -m 600 -o root /dev/null /etc/dataproductfactory.env   # then fill it in

The minimum for a deployment behind a host site's sign-in:

DPF_ENV=production
DPF_PREFIX=/dataproductfactory
SECRET_KEY=…            # the site's own key, so its session cookie can be read
DPF_IDENTITY=site+link  # see ../architecture/identity.md
ALLOWED_EMAILS=…
DPF_SITE_URL=https://…
DPF_LINK_KEY=…          # a long random string

3. Install and start the service:

sudo cp deploy/dataproductfactory.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now dataproductfactory

4. Paste deploy/nginx-location.conf into the site's server { listen 443 … } block, above the catch-all location /, and reload nginx.

Releasing a change

1. Merge to the deployed branch, with the suite green locally.

2. Back up what will be replaced, including the database — see Back up and restore:

cp -a /srv/dataproductfactory/{dpf,templates,static,var/dpf.sqlite3} /root/deploy-backups/<date>/

3. Ship the code — either git pull on the server, which keeps .git so the running build reads straight from it, or, from a checkout, write the commit into a BUILD file next to VERSION and rsync both alongside the application:

git rev-parse --short HEAD > BUILD
rsync -a --exclude __pycache__ dpf templates static tests docs README.md CHANGELOG.md requirements.txt VERSION BUILD web@host:/srv/dataproductfactory/

Without .git on the server, BUILD is the only way the app can say which commit is running (dpf/version.py) — skip it and the build mark goes blank, though the version number still updates.

4. Run ./deploy/deploy.sh — it installs requirements.txt, runs the test suite, and restarts — or do those three by hand: .venv/bin/pip install -r requirements.txt, the suite, then systemctl restart dataproductfactory. deploy.sh also writes BUILD from git itself, but only when .git is there to ask — after a git pull that is redundant and harmless; after an rsync it is a silent no-op, which is why the step above does it by hand first.

5. Check it: the app answers, and systemctl is-active dataproductfactory.

Schema changes need no step of their own: every start adds whatever tables or columns are missing (dpf/db.py, _migrate), so a change appears after the restart. Nothing is ever dropped automatically.

Operating it

Every command runs as the service account, with its environment loaded:

cd /srv/dataproductfactory
set -a; . /etc/dataproductfactory.env; set +a
sudo -E -u web .venv/bin/python -m dpf <command>
Task Command
Add a person, or recover a locked-out administrator user add\|invite … — see Add a person
Issue or revoke a read-only API token token create\|list\|revoke … — see Issue an API token
Cut a release release major\|minor\|patch — see Release a version
Rebuild the demo products showcase
Check the warehouse connection connect
Export every document as markdown export PRODUCT --to DIR
Validate a workflow file before using it workflow FILE

Logs go to the journal: journalctl -u dataproductfactory -f.

Check that it worked

  • systemctl is-active dataproductfactory says active.
  • The app answers, and the sidebar foot shows the version — and, once deployed by the rsync path above, the build — you shipped.
  • journalctl -u dataproductfactory -n 20 has no traceback since the restart.

If it does not work

  • The service refuses to start, and the journal names a variable. Startup refuses an incomplete or contradictory identity configuration rather than guessing — the message names what is missing or conflicting; fix /etc/dataproductfactory.env and restart.
  • The site serves the old version after a deploy. deploy.sh leaves the old version running when the test suite fails — check its output, then journalctl -u dataproductfactory.
  • 404 on every page under the prefix. nginx-location.conf's proxy_pass has no trailing slash on purpose — adding one strips the prefix and breaks every URL the app builds with DPF_PREFIX.
Documentation for version 0.2.0 Reviewed 2026-10-04 Covers deploy/*, dpf/version.py