diff --git a/README.md b/README.md index 1d7ff34..067b5e3 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,37 @@ The `sqlite_fts5` tag is needed for the database's title search (see [Testing](#testing)). +## Running as a service +`deploy/job-search.service` is a systemd unit running `job-search serve` +(the server and every enabled source's fetcher) as a `job-search` system +user, from `/usr/local/bin/job-search` with `/etc/job-search/config.yaml`. +Its comments give the install steps. It keeps its data in +`/var/lib/job-search` and its log in `/var/log/job-search`, the only places +it can write, so set `database.file` and `logging.file` there. + +- The database isn't migrated on start: run `job-search migrate up` + (`--create` the first time) as the `job-search` user before the first + start and after each upgrade. +- A failing source stops `serve` on purpose; the unit restarts it after + 30s. + +### Rotating the logs +`job-search` opens its log file once and has no signal to reopen it, so +logrotate must copy the file and truncate it in place (`copytruncate`); a +line written between the two is lost. `deploy/job-search.logrotate` does +this weekly for every `*.log` in `/var/log/job-search` (the log and, if +`server.access_log` is set there, the access log), keeping 8 weeks, +compressed. `/var/log/job-search` belongs to the service user, so it rotates +as that user (`su job-search job-search`); logrotate refuses otherwise. + +```bash +# from the release tarball's directory (deploy/job-search.logrotate in the repo) +sudo install -m 644 job-search.logrotate /etc/logrotate.d/job-search +sudo logrotate --debug /etc/logrotate.d/job-search # dry run: what it would do +sudo logrotate --force /etc/logrotate.d/job-search # rotate now, to check +``` + + ## Configuration Configuration is read from, in increasing priority: 1. Built-in defaults @@ -212,6 +243,7 @@ second one fails with "address already in use". | `internal/version` | Version information set at build time | | `internal/frontend` | Serves the frontend's build output; embeds `dist/`, copied there from `web/dist` by `npm run sync` | | `web` | The frontend (see [web](web/README.md)) | +| `deploy` | The systemd unit and logrotate configuration (see [Running as a service](#running-as-a-service)) | ## Testing diff --git a/deploy/job-search.logrotate b/deploy/job-search.logrotate new file mode 100644 index 0000000..65953af --- /dev/null +++ b/deploy/job-search.logrotate @@ -0,0 +1,18 @@ +# logrotate configuration for job-search's logs (see README.md, "Rotating +# the logs"). Install as /etc/logrotate.d/job-search; it is in the release +# tarball next to the binary. +# +# job-search opens its log file once and has no signal to reopen it, so the +# file is copied and then truncated in place (copytruncate). A line written +# between the copy and the truncation is lost. +/var/log/job-search/*.log { + weekly + rotate 8 + compress + delaycompress + missingok + notifempty + copytruncate + # /var/log/job-search belongs to the service user, so rotate as it. + su job-search job-search +} diff --git a/deploy/job-search.service b/deploy/job-search.service new file mode 100644 index 0000000..7e3b154 --- /dev/null +++ b/deploy/job-search.service @@ -0,0 +1,80 @@ +# job-search: serves the API and frontend, and runs the enabled sources' +# fetchers (job-search serve). See README.md, "Running as a service". +# +# Install, from the release tarball's directory (job-search--): +# sudo useradd --system --home-dir /var/lib/job-search --shell /usr/sbin/nologin job-search +# sudo install -m 755 job-search /usr/local/bin/job-search +# sudo install -d -m 750 -o root -g job-search /etc/job-search +# sudo install -m 640 -o root -g job-search config.yaml /etc/job-search/config.yaml +# sudo install -d -m 750 -o job-search -g job-search /var/lib/job-search /var/log/job-search +# sudo -u job-search /usr/local/bin/job-search --config /etc/job-search/config.yaml migrate up --create +# sudo install -m 644 job-search.service /etc/systemd/system/ +# sudo systemctl daemon-reload +# sudo systemctl enable --now job-search +# +# /etc/job-search/config.yaml should set at least: +# database: +# file: /var/lib/job-search/jobsearch.db +# logging: +# file: /var/log/job-search/job-search.log +# Without logging.file, logs go to the journal (as coloured text). Rotate the +# log file with job-search.logrotate: +# sudo install -m 644 job-search.logrotate /etc/logrotate.d/job-search +# +# After each upgrade, migrate the database before restarting: +# sudo -u job-search /usr/local/bin/job-search --config /etc/job-search/config.yaml migrate up +# sudo systemctl restart job-search + +[Unit] +Description=Job Search: procurement opportunities server and fetcher +Documentation=https://git.avsdev.uk/Go/job-search +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +User=job-search +Group=job-search +# /var/lib/job-search and /var/log/job-search, created and owned by the +# service user; the only places it can write. +StateDirectory=job-search +StateDirectoryMode=0750 +LogsDirectory=job-search +LogsDirectoryMode=0750 +WorkingDirectory=/var/lib/job-search +ExecStart=/usr/local/bin/job-search --config /etc/job-search/config.yaml serve +# serve shuts down cleanly on SIGTERM: the HTTP server drains and in-flight +# fetches get up to 10s. +KillSignal=SIGTERM +TimeoutStopSec=30 +# A failing source stops serve on purpose; restart to retry it. +Restart=on-failure +RestartSec=30 +UMask=0027 + +# Hardening +NoNewPrivileges=yes +CapabilityBoundingSet= +AmbientCapabilities= +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +ProtectClock=yes +ProtectHostname=yes +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallFilter=~@privileged @resources + +[Install] +WantedBy=multi-user.target diff --git a/scripts/build.sh b/scripts/build.sh index b874c9d..c534fd8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -62,12 +62,17 @@ go build \ echo "Built $BUILD_DIR/job-search-$VERSION" -# If proper release, copy to output directory for packaging +# If proper release, copy to output directory for packaging, with the +# systemd unit and logrotate configuration (see README.md, "Running as a +# service") if [[ $DO_RELEASE -eq 1 ]]; then install -DT $BUILD_DIR/job-search-$VERSION $RELEASE_DIR/job-search + install -DT -m 644 deploy/job-search.service $RELEASE_DIR/job-search.service + install -DT -m 644 deploy/job-search.logrotate $RELEASE_DIR/job-search.logrotate ARCH=`dpkg --print-architecture` tar -czf $RELEASE_DIR/job-search-$VERSION-$ARCH.tar.gz \ --transform "s,^$RELEASE_DIR/,job-search-$VERSION-$ARCH/," \ - $RELEASE_DIR/job-search + $RELEASE_DIR/job-search $RELEASE_DIR/job-search.service \ + $RELEASE_DIR/job-search.logrotate echo "Installed $RELEASE_DIR/job-search" fi