First time setup
You give the machine what every dev facility needs and no facility owns: a database engine and a mail catcher, both containers; the settings every facility shares; the services that run a facility; and the web server's own configuration. Each of these is done once per machine. At the end of this page the machine can carry dev facilities, and has none.
Every command on this page runs from the root of the checkout.
Start the containers
The database engine and the mail catcher are two containers, defined in the checkout's
docker-compose.yml. Start them:
cd "$ZFM_ROOT_DIR"
docker compose up -d
docker compose ps
Wait for both to read healthy. They restart on their own after a reboot, so this is done once.
What you now have, and where it answers:
| MySQL 8.4, the same engine and version production runs | 127.0.0.1:3308, as root with the admin db password rootpass |
| Mailpit, an SMTP server that accepts every message and delivers none | SMTP on localhost:1025; the inbox at http://localhost:8025 |
The catcher is not a convenience. A dev facility is built from a real facility's records, so its users are real researchers with real email addresses, and an invitation or a password reset sent from this machine with a working mail account would reach one of them. Every message a dev facility sends lands in that inbox instead, where you can read it. It is also the only place to see the link in an emailed message, which is how you check that a facility's address is right.
Both ports answer only on this machine's loopback address. The compose file prefixes every
published port with 127.0.0.1: because Docker's own firewall rules run before the host's, so a
port published on every interface is reachable from the network whatever the host's firewall says.
Point the server at the checkout's settings
A production host keeps its settings in /etc/zfm. A development machine keeps them in the
checkout, in zf-server/environments/, which is gitignored except for the two templates in it.
The server looks in /etc/zfm unless told otherwise, so tell the shell you are in, and then, in
bash, every shell after it:
export ZFM_CONFIG_DIR=$ZFM_ROOT_DIR/zf-server/environments
# persist ZFM_CONFIG_DIR
echo "export ZFM_CONFIG_DIR=$ZFM_CONFIG_DIR" >> ~/.bashrc
The services that run a facility carry the same setting themselves, so this is for the commands you type.
Write the shared settings
Every facility on this machine reads one file for what they all have in common: the database server, the mail account, who owns the platform. Start it from the template beside it, which documents every key the file accepts:
cd "$ZFM_ROOT_DIR"
cp zf-server/environments/sample.zfm.conf zf-server/environments/zfm.conf
Then edit it. The template holds a production host's values, and four things change on a development machine:
NODE_ENV |
development. A server that does not say so is treated as production, and the client shows a warning watermark over a development one. |
| the database server | DB_HOST=127.0.0.1 and DB_PORT=3308, the container. Delete DB_SSL_CA: there is no certificate on a local connection. |
| the mail settings | The template has eight MAIL_ keys. Set three to the catcher: MAIL_HOST=localhost, MAIL_PORT=1025, MAIL_TLS_MODE=none. Delete two: MAIL_USER and MAIL_PASSWORD, because the catcher takes no credentials. Keep the other three, MAIL_FROM, MAIL_REPLY_TO and MAIL_CC, with any addresses: they have to be present, and with a mail catcher their values have no effect, since nothing is delivered. |
| the platform owner | SUPER_USERNAME, SUPER_NAME, SUPER_INITIALS, SUPER_EMAIL: you. |
Every other key in the file keeps the template's value. Nothing else is deleted.
Am I ready to move on?
The file is written by hand, and the first thing that normally reads it is a server starting up. This reads it the same way and reports every fault at once: a missing, misspelled or extraneous key, or a value that is not the kind of value it should be.
cd "$ZFM_ROOT_DIR/zf-server" && npm run check-config-syntax -- --system
It also lists the keys a facility's own file still has to state, which is expected: those are particular to a facility, and come on the next page.
Install the services
A facility's server, the build of the contract it depends on, and the client's bundler each run as a systemd user service: supervised, started at login, restarted when they die, and writing everything they print to a journal you can read. The three unit files are in the checkout and are copied into your own systemd directory:
install -d ~/.config/systemd/user
cp deploy/systemd/user/zfm-shared.service \
deploy/systemd/user/zfm-server@.service \
deploy/systemd/user/zfm-client.service \
~/.config/systemd/user/
The unit files know nothing about this machine: not where the checkout is, and not where Node
is, because a service does not run your shell and so sees neither ZFM_ROOT_DIR nor nvm's Node.
Each value that belongs to the machine goes in a drop-in beside its unit, written here from the
variable and from the Node directory you collected on Prepare the machine:
# each unit works in its own part of the checkout, and finds npm where nvm put node
for entry in zfm-shared.service: zfm-server@.service:/zf-server zfm-client.service:/zf-react-client; do
unit=${entry%%:*}; subdir=${entry#*:}
install -d ~/.config/systemd/user/$unit.d
printf '[Service]\nWorkingDirectory=%s\nEnvironment=PATH=%s:/usr/local/bin:/usr/bin:/bin\n' \
"$ZFM_ROOT_DIR$subdir" "$(dirname "$(command -v node)")" > ~/.config/systemd/user/$unit.d/machine.conf
done
# the server also reads its settings from the checkout
printf 'Environment=ZFM_CONFIG_DIR=%s\n' "$ZFM_CONFIG_DIR" >> ~/.config/systemd/user/zfm-server@.service.d/machine.conf
When the checkout moves or the Node version changes, this is the one place to update.
Then tell systemd about them, and run the one that has no facility to wait for:
systemctl --user daemon-reload
systemctl --user start zfm-shared
systemctl --user status zfm-shared
active (exited) is the right answer: it built the contract and is done. A failure, and what the
build printed, is in journalctl --user -u zfm-shared.
Give the web server its configuration
Every facility on this machine is served by Caddy at an address of its own, from a small file per facility. The host's own Caddyfile does one thing, which is to read those files, and a facility never edits it:
sudo install -d -o root -g root -m 755 /etc/caddy/conf.d
printf 'import /etc/caddy/conf.d/*.caddy\n' | sudo tee /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
That replaces the Caddyfile the package installed, which served a welcome page on port 80. The machine now serves nothing, because the directory is empty, and each facility adds one file to it when it is put on the air.
This is the layout a production host has. A facility's file is the same shape on both, with one difference inside it, and the file is the unit a person reads, writes and removes.
Am I ready to move on?
cd "$ZFM_ROOT_DIR/zf-server" && npm test
Green means the container answers, the whole migration chain builds a schema from nothing, and the server compiles against the contract you built. Expect one skipped suite. Nothing here needs a facility: each test worker makes a throwaway schema of its own on the container and drops it at the end.
systemctl --user status zfm-shared # active (exited)
sudo caddy validate --config /etc/caddy/Caddyfile