Bookstack bundle

This commit is contained in:
Daniel Berteaud 2024-01-21 22:01:33 +01:00
parent b0845b9b0b
commit 3ff8bf1567
26 changed files with 692 additions and 0 deletions

93
bookstack.nomad.hcl Normal file
View File

@ -0,0 +1,93 @@
job "[[ .instance ]]" {
[[- $c := merge .bookstack . ]]
[[ template "common/job_start" $c ]]
group "bookstack" {
count = [[ $c.count ]]
network {
mode = "bridge"
}
[[ template "common/volumes" $c ]]
service {
name = "[[ .instance ]][[ .consul.suffix ]]"
port = 8080
[[ template "common/connect" $c ]]
tags = [
"[[ $c.traefik.instance ]].enable=[[ $c.traefik.enabled ]]",
# Define a middleware to set custom CSP headers
"[[ $c.traefik.instance ]].http.middlewares.[[ .instance ]]-headers[[ .consul.suffix ]].headers.contentsecuritypolicy=[[ range $k, $v := $c.traefik.csp ]][[ $k ]] [[ $v ]];[[ end ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].rule=Host(`[[ (urlParse $c.public_url).Hostname ]]`)
[[- if not (regexp.Match "^/?$" (urlParse $c.public_url).Path) ]] && PathPrefix(`[[ (urlParse $c.public_url).Path ]]`)[[ end ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].entrypoints=[[ join $c.traefik.entrypoints "," ]]",
[[- if not (regexp.Match "^/?$" (urlParse $c.public_url).Path) ]]
"[[ $c.traefik.instance ]].http.middlewares.[[ .instance ]]-prefix[[ .consul.suffix ]].stripprefix.prefixes=[[ (urlParse $c.public_url).Path ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].middlewares=[[ .instance ]]-headers[[ .consul.suffix ]],[[ .instance ]]-prefix[[ .consul.suffix ]],[[ template "common/traefik_middlewares" $c ]]",
[[- else ]]
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].middlewares=[[ .instance ]]-headers[[ .consul.suffix ]],[[ template "common/traefik_middlewares" $c ]]"
[[- end ]]
]
}
task "bookstack" {
driver = "[[ $c.nomad.driver ]]"
config {
image = "[[ $c.image ]]"
pids_limit = 100
readonly_rootfs = true
volumes = ["secrets/bookstack.env:/app/.env"]
[[ template "common/tmpfs" dict "size" "5000000" "target" "/tmp" ]]
}
env {
[[ template "common/proxy_env" $c ]]
}
[[ template "common/vault.policies" $c ]]
[[ template "common/file_env" $c ]]
# Ensure only the first allocation runs the database initialization / upgrades
template {
data = <<_EOT
{{- if eq (env "NOMAD_ALLOC_INDEX") "0" }}
BOOKSTACK_INIT_DB=true
{{- else }}
MINIT_DISABLE=bookstack-queue
BOOKSTACK_INIT_DB=false
{{- end }}
_EOT
destination = "secrets/bookstack_initdb.env"
env = true
}
# BookStack won't work with env vars because PHP clears env
# So, publish BookStack settings here
template {
data =<<_EOT
[[- range $k, $v := $c.settings ]]
[[ $k ]]=[[ $v ]]
[[- end ]]
_EOT
destination = "secrets/bookstack.env"
uid = 100100
gid = 100000
perms = 400
}
volume_mount {
volume = "data"
destination = "/data"
}
[[ template "common/resources" $c ]]
}
}
}

4
bundles.yml Normal file
View File

@ -0,0 +1,4 @@
---
dependencies:
- url: ../common.git

View File

@ -0,0 +1,3 @@
Kind = "service-defaults"
Name = "[[ .instance ]][[ .consul.suffix ]]"
Protocol = "http"

View File

@ -0,0 +1,16 @@
Kind = "service-intentions"
Name = "[[ .instance ]][[ .consul.suffix ]]"
Sources = [
{
Name = "[[ (merge .bookstack .).traefik.instance ]]"
Permissions = [
{
Action = "allow"
HTTP {
PathPrefix = "/"
Methods = ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"]
}
}
]
}
]

9
example/LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 dani
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
example/README.md Normal file
View File

@ -0,0 +1,3 @@
# bookstack
Nomad job template for BookStack

165
example/bookstack.nomad.hcl Normal file
View File

@ -0,0 +1,165 @@
job "bookstack" {
datacenters = ["dc1"]
group "bookstack" {
count = 1
network {
mode = "bridge"
}
volume "data" {
source = "bookstack-data"
type = "csi"
access_mode = "multi-node-multi-writer"
attachment_mode = "file-system"
}
service {
name = "bookstack"
port = 8080
connect {
sidecar_service {
proxy {
upstreams {
destination_name = "mariadb"
local_bind_port = 3306
}
upstreams {
destination_name = "smtp"
local_bind_port = 25
}
}
}
sidecar_task {
resources {
cpu = 50
memory = 64
}
}
}
tags = [
"traefik.enable=true",
# Define a middleware to set custom CSP headers
"traefik.http.middlewares.bookstack-headers.headers.contentsecuritypolicy=default-src 'self';font-src 'self' data:;img-src 'self' blob: data:;script-src 'self' 'unsafe-inline';style-src 'self' 'unsafe-inline';",
"traefik.http.routers.bookstack.rule=Host(`bookstack.example.org`)",
"traefik.http.routers.bookstack.entrypoints=https",
"traefik.http.routers.bookstack.middlewares=bookstack-headers,rate-limit-std@file,inflight-std@file,security-headers@file,hsts@file,compression@file"
]
}
task "bookstack" {
driver = "docker"
config {
image = "danielberteaud/bookstack:latest"
pids_limit = 100
readonly_rootfs = true
volumes = ["secrets/bookstack.env:/app/.env"]
mount {
type = "tmpfs"
target = "/tmp"
tmpfs_options {
size = 5000000
}
}
}
env {
}
vault {
policies = ["bookstack"]
env = false
disable_file = true
}
# Use a template block instead of env {} so we can fetch values from vault
template {
data = <<_EOT
LANG=fr_FR.utf8
TZ=Europe/Paris
WAIT_FOR_TARGET=127.0.0.1:3306
_EOT
destination = "secrets/.env"
perms = 400
env = true
}
# Ensure only the first allocation runs the database initialization / upgrades
template {
data = <<_EOT
{{- if eq (env "NOMAD_ALLOC_INDEX") "0" }}
BOOKSTACK_INIT_DB=true
{{- else }}
MINIT_DISABLE=bookstack-queue
BOOKSTACK_INIT_DB=false
{{- end }}
_EOT
destination = "secrets/bookstack_initdb.env"
env = true
}
# BookStack won't work with env vars because PHP clears env
# So, publish BookStack settings here
template {
data = <<_EOT
APP_KEY={{ with secret "kv/service/bookstack" }}{{ .Data.data.app_key }}{{ end }}
APP_LANG=fr
APP_PROXIES=127.0.0.1
APP_URL=https://bookstack.example.org
CACHE_DRIVER=database
CACHE_PREFIX=bookstack
DB_DATABASE=bookstack
DB_HOST=127.0.0.1
DB_PASSWORD={{ with secret "database/creds/bookstack" }}{{ .Data.password }}{{ end }}
DB_PORT=3306
DB_USERNAME={{ with secret "database/creds/bookstack" }}{{ .Data.username }}{{ end }}
DISABLE_EXTERNAL_SERVICES=true
MAIL_DRIVER=smtp
MAIL_FROM=no-reply@bookstack.example.org
MAIL_FROM_NAME=bookstack
MAIL_HOST=127.0.0.1
MAIL_PORT=25
QUEUE_CONNECTION=database
SESSION_COOKIE_NAME=bookstack_session
SESSION_DRIVER=database
SESSION_LIFETIME=240
STORAGE_TYPE=local_secure_restricted
WKHTMLTOPDF=/usr/local/bin/wkhtmltopdf
_EOT
destination = "secrets/bookstack.env"
uid = 100100
gid = 100000
perms = 400
}
volume_mount {
volume = "data"
destination = "/data"
}
resources {
cpu = 100
memory = 256
memory_max = 512
}
}
}
}

View File

@ -0,0 +1,3 @@
Kind = "service-defaults"
Name = "bookstack"
Protocol = "http"

View File

@ -0,0 +1,16 @@
Kind = "service-intentions"
Name = "bookstack"
Sources = [
{
Name = "traefik"
Permissions = [
{
Action = "allow"
HTTP {
PathPrefix = "/"
Methods = ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"]
}
}
]
}
]

View File

@ -0,0 +1,57 @@
FROM danielberteaud/php:83.24.1-4
MAINTAINER Daniel Berteaud <dbd@ehtrace.com>
ARG BOOKSTACK_VERSION=23.12.1
ENV LANG=fr_FR.utf8 \
TZ=Europe/Paris \
APP_ROOT=/app/public \
BOOKSTACK_INIT_DB=true \
WKHTMLTOPDF=/usr/local/bin/wkhtmltopdf \
APP_ENV=production \
APP_TIMEZONE='Europe/Paris' \
SESSION_SECURE_COOKIE=true
USER root
COPY --from=danielberteaud/wkhtmltopdf:24.1-1 /usr/local/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
RUN set -euxo pipefail &&\
apk add --no-cache \
php${PHP_VERSION}-tokenizer \
php${PHP_VERSION}-simplexml \
&&\
# For wkhtml2pdf \
apk add --no-cache \
libstdc++ \
libx11 \
libxrender \
libxext \
ca-certificates \
fontconfig \
freetype \
ttf-dejavu \
ttf-droid \
ttf-freefont \
ttf-liberation \
&&\
apk add --no-cache --virtual .build-deps \
msttcorefonts-installer \
&&\
# Install microsoft fonts \
update-ms-fonts &&\
fc-cache -f &&\
# Clean up when done \
rm -rf /tmp/* &&\
apk del .build-deps &&\
curl -sSL https://github.com/BookStackApp/BookStack/archive/refs/tags/v${BOOKSTACK_VERSION}.tar.gz |\
tar xvz -C /app --strip-components 1 &&\
cd /app/ &&\
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev &&\
rm -rf storage bootstrap/cache public/uploads &&\
ln -sf /data/app/storage storage &&\
ln -sf /data/app/cache bootstrap/cache &&\
ln -s /data/app/uploads public/uploads
COPY root/ /
USER nginx

View File

@ -0,0 +1,17 @@
#!/bin/sh
umask 077
for DIR in app \
backups \
clockwork \
fonts \
framework/cache \
frameworks/sessions \
framework/views \
logs \
uploads/files \
uploads/images \
; do
mkdir -p /data/app/storage/${DIR}
done

View File

@ -0,0 +1,23 @@
#!/bin/sh
set -euo pipefail
if [ "${BOOKSTACK_INIT_DB}" = "true" ]; then
if [ ! -e /data/version ]; then
echo "Initilizing database"
php /app/artisan migrate --force
cat /app/version > /data/version
elif [ "$(cat /app/version)" != "$(cat /data/version)" ]; then
echo "Upgrade from $(cat /data/version) to $(cat /app/version)"
php /app/artisan migrate --force
echo "Clearing cache"
php /app/artisan cache:clear
echo "Clearing views"
php /app/artisan view:clear
echo "Regenerating search"
php /app/artisan bookstack:regenerate-search
cat /app/version > /data/version
else
echo "No version change, nothing to do"
fi
fi

View File

@ -0,0 +1,5 @@
---
kind: daemon
name: bookstack-queue
command: ["php", "/app/artisan", "queue:work", "--sleep=3", "--tries=3", "--max-time=3600"]

12
example/init/vault-database Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
set -euo pipefail
vault write database/roles/bookstack \
db_name="mariadb" \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \
GRANT ALL PRIVILEGES ON bookstack.* TO '{{name}}'@'%'; \
FLUSH PRIVILEGES;" \
default_ttl="12h" \
max_ttl="720h"

19
example/prep.d/10-mv-conf.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
set -eu
if [ "bookstack" != "bookstack" ]; then
for DIR in vault consul nomad; do
if [ -d output/${DIR} ]; then
for FILE in $(find output/${DIR} -name "*bookstack*.hcl" -type f); do
NEW_FILE=$(echo "${FILE}" | sed -E "s/bookstack/bookstack/g")
mv "${FILE}" "${NEW_FILE}"
done
fi
done
fi

18
example/prep.d/20-rand-pwd.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/sh
set -euo pipefail
VAULT_KV_PATH=kv/service/bookstack
RAND_CMD="echo base64:$(openssl rand -base64 32)"
if ! vault kv list $(dirname ${VAULT_KV_PATH}) 2>/dev/null | grep -q -E "^$(basename ${VAULT_KV_PATH})\$"; then
vault kv put ${VAULT_KV_PATH} \
app_key=$(${RAND_CMD}) \
fi
for SECRET in app_key; do
if ! vault kv get -field ${SECRET} ${VAULT_KV_PATH} >/dev/null 2>&1; then
vault kv patch ${VAULT_KV_PATH} \
${SECRET}=$(${RAND_CMD})
fi
done

View File

@ -0,0 +1,7 @@
path "database/creds/bookstack" {
capabilities = ["read"]
}
path "kv/data/service/bookstack" {
capabilities = ["read"]
}

View File

@ -0,0 +1,57 @@
FROM [[ .docker.repo ]][[ .docker.base_images.php83.image ]]
MAINTAINER [[ .docker.maintainer ]]
ARG BOOKSTACK_VERSION=23.12.1
ENV LANG=[[ .locale.lang ]] \
TZ=[[ .locale.tz ]] \
APP_ROOT=/app/public \
BOOKSTACK_INIT_DB=true \
WKHTMLTOPDF=/usr/local/bin/wkhtmltopdf \
APP_ENV=production \
APP_TIMEZONE='[[ .locale.tz ]]' \
SESSION_SECURE_COOKIE=true
USER root
COPY --from=[[ .docker.repo ]][[ .docker.base_images.wkhtmltopdf.image ]] /usr/local/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
RUN set -euxo pipefail &&\
apk add --no-cache \
php${PHP_VERSION}-tokenizer \
php${PHP_VERSION}-simplexml \
&&\
# For wkhtml2pdf \
apk add --no-cache \
libstdc++ \
libx11 \
libxrender \
libxext \
ca-certificates \
fontconfig \
freetype \
ttf-dejavu \
ttf-droid \
ttf-freefont \
ttf-liberation \
&&\
apk add --no-cache --virtual .build-deps \
msttcorefonts-installer \
&&\
# Install microsoft fonts \
update-ms-fonts &&\
fc-cache -f &&\
# Clean up when done \
rm -rf /tmp/* &&\
apk del .build-deps &&\
curl -sSL https://github.com/BookStackApp/BookStack/archive/refs/tags/v${BOOKSTACK_VERSION}.tar.gz |\
tar xvz -C /app --strip-components 1 &&\
cd /app/ &&\
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev &&\
rm -rf storage bootstrap/cache public/uploads &&\
ln -sf /data/app/storage storage &&\
ln -sf /data/app/cache bootstrap/cache &&\
ln -s /data/app/uploads public/uploads
COPY root/ /
USER nginx

View File

@ -0,0 +1,17 @@
#!/bin/sh
umask 077
for DIR in app \
backups \
clockwork \
fonts \
framework/cache \
frameworks/sessions \
framework/views \
logs \
uploads/files \
uploads/images \
; do
mkdir -p /data/app/storage/${DIR}
done

View File

@ -0,0 +1,23 @@
#!/bin/sh
set -euo pipefail
if [ "${BOOKSTACK_INIT_DB}" = "true" ]; then
if [ ! -e /data/version ]; then
echo "Initilizing database"
php /app/artisan migrate --force
cat /app/version > /data/version
elif [ "$(cat /app/version)" != "$(cat /data/version)" ]; then
echo "Upgrade from $(cat /data/version) to $(cat /app/version)"
php /app/artisan migrate --force
echo "Clearing cache"
php /app/artisan cache:clear
echo "Clearing views"
php /app/artisan view:clear
echo "Regenerating search"
php /app/artisan bookstack:regenerate-search
cat /app/version > /data/version
else
echo "No version change, nothing to do"
fi
fi

View File

@ -0,0 +1,5 @@
---
kind: daemon
name: bookstack-queue
command: ["php", "/app/artisan", "queue:work", "--sleep=3", "--tries=3", "--max-time=3600"]

8
init/vault-database Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -euo pipefail
[[- template "common/vault.mkmysqlrole.sh"
dict "ctx" .
"config" (dict "role" .instance "database" "mariadb")
]]

1
prep.d/10-mv-conf.sh Executable file
View File

@ -0,0 +1 @@
[[ template "common/mv_conf.sh" dict "ctx" . "services" (dict "bookstack" .instance) ]]

5
prep.d/20-rand-pwd.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
set -euo pipefail
[[ template "common/vault.rand_secrets" dict "ctx" . "keys" (coll.Slice "app_key") "cmd" "echo base64:$(openssl rand -base64 32)" ]]

99
variables.yml Normal file
View File

@ -0,0 +1,99 @@
---
# Name of this instance
instance: bookstack
bookstack:
# Docker image to use
image: '[[ .docker.repo ]]bookstack:23.12.1'
# Number of instances to run
count: 1
# Custom env vars to set in the container
env:
WAIT_FOR_TARGET: 127.0.0.1:3306
# Resource allocation
resources:
cpu: 100
memory: 256
memory_max: 512
vault:
# List of vault policies to attach in to the task
policies:
- '[[ .instance ]][[ .consul.suffix ]]'
# Wait for MariaDB to be ready before starting
wait_for:
service: mariadb[[ .consul.suffix ]]
consul:
connect:
# Connect a few services from the mesh
upstreams:
- destination_name: mariadb[[ .consul.suffix ]]
local_bind_port: 3306
- destination_name: '[[ .mail.smtp_service_name ]]'
local_bind_port: 25
# Bookstack settings (which will populate .env)
settings:
APP_KEY: '{{ with secret "[[ .vault.prefix ]]kv/service/[[ .instance ]]" }}{{ .Data.data.app_key }}{{ end }}'
APP_URL: '[[ .bookstack.public_url ]]'
APP_LANG: fr
APP_PROXIES: 127.0.0.1
DB_HOST: '[[ .bookstack.mysql.host ]]'
DB_PORT: '[[ .bookstack.mysql.port ]]'
DB_DATABASE: '[[ .bookstack.mysql.database ]]'
DB_USERNAME: '[[ .bookstack.mysql.user ]]'
DB_PASSWORD: '[[ .bookstack.mysql.password ]]'
MAIL_DRIVER: smtp
MAIL_FROM_NAME: '[[ .instance ]]'
MAIL_FROM: no-reply@[[ (urlParse .bookstack.public_url).Hostname ]]
MAIL_HOST: 127.0.0.1
MAIL_PORT: 25
CACHE_PREFIX: '[[ .instance ]]'
CACHE_DRIVER: database
SESSION_DRIVER: database
SESSION_LIFETIME: 240
SESSION_COOKIE_NAME: '[[ .instance ]]_session'
QUEUE_CONNECTION: database
STORAGE_TYPE: local_secure_restricted
DISABLE_EXTERNAL_SERVICES: true
WKHTMLTOPDF: /usr/local/bin/wkhtmltopdf
# Database settings
mysql:
host: 127.0.0.1
port: 3306
database: '[[ .instance ]]'
user: '{{ with secret "[[ .vault.prefix ]]database/creds/[[ .instance ]]" }}{{ .Data.username }}{{ end }}'
password: '{{ with secret "[[ .vault.prefix ]]database/creds/[[ .instance ]]" }}{{ .Data.password }}{{ end }}'
# Public URL on which bookstack will be available
public_url: https://bookstack.example.org
# Traefik settings
traefik:
middlewares:
# Can be removed once compression is added back to the default base_middlewares
- compression@file
# Custom CSP to set
csp:
default-src: "'self'"
script-src: "'self' 'unsafe-inline'"
img-src: "'self' blob: data:"
style-src: "'self' 'unsafe-inline'"
font-src: "'self' data:"
# Volume for data persistence
volumes:
data:
type: csi
source: '[[ .instance ]]-data[[ .consul.suffix ]]'
# Note: use multi-node-multi-writer so we can run several instances
access_mode: multi-node-multi-writer

View File

@ -0,0 +1,7 @@
path "[[ .vault.prefix ]]database/creds/[[ .instance ]]" {
capabilities = ["read"]
}
path "[[ .vault.prefix ]]kv/data/service/bookstack" {
capabilities = ["read"]
}