diff --git a/.env.dist b/.env.dist index b2133b0..13f0056 100644 --- a/.env.dist +++ b/.env.dist @@ -1,11 +1,15 @@ -# define the path of your web application, relative to the current folder -WWW_ROOT=../data +# fewohbee configuration +# Copy this file to .env and adjust the values accordingly. +# New variables will be added automatically by update-docker.sh. + +# Compose file to use. Change to docker-compose.no-ssl.yml for reverse proxy mode. +COMPOSE_FILE=docker-compose.yml # set timezone TZ=Europe/Berlin # e.g. fewohbee or mydomain.tld -HOST_NAME=fewohbee +HOST_NAME=fewohbee # mysql settings MARIADB_ROOT_PASSWORD= @@ -16,14 +20,10 @@ MARIADB_DATABASE=fewohbee MYSQL_BACKUP_USER=backupuser MYSQL_BACKUP_PASSWORD= -MYSQL_BACKUP_FOLDER=../dbbackup - -DOCKER_API_VERSION=1.37 - # letsencrypt settings LETSENCRYPT=false EMAIL="" -# enter here all (sub-)domains which should be included in the certificate, sepearated with a whitespace e.g.: domain.tld sub1.domain.tld +# enter here all (sub-)domains which should be included in the certificate, separated with a whitespace e.g.: domain.tld sub1.domain.tld LETSENCRYPT_DOMAINS="" # if used specify your dyndns provider, currently "desec.io" is supported # leave empty if not used @@ -33,11 +33,15 @@ DEDYN_TOKEN="" # Set your dedyn.io domain name here: DEDYN_NAME="" - # self signed certificate settings SELF_SIGNED=true -# FewohBee Settings +# reverse proxy settings (docker-compose.no-ssl.yml) +# Port exposed by the web container when using docker-compose.no-ssl.yml +LISTEN_PORT=80 + +###> Application settings ### + LOCALE=de FEWOHBEE_VERSION=latest APP_ENV=prod @@ -77,3 +81,5 @@ PASSKEY_ENABLED=false # otherwise the host name of your web server must be set, e.g. https://pve # leave this untouched when using fewohbee-dockerized WEB_HOST=http://web:8080 + +###< Application settings ### diff --git a/.gitignore b/.gitignore index 03b0331..20a1505 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env +.env.app .DS_Store conf/nginx/server_name.active diff --git a/README.md b/README.md index 35125d0..37ed9c5 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,99 @@ +# fewohbee-dockerized - # fewohbee-dockerized +This docker compose setup is part of the [fewohbee guesthouse administration tool](https://github.com/developeregrem/fewohbee). It provides all necessary services to run fewohbee out of the box. -This docker-compose setup is part of the [guesthouse administration tool](https://github.com/developeregrem/fewohbee). fewohbee-dockerized provides all necessary software/images in order to run the guesthouse administration tool (Pensionsverwaltung) out of the box. +## Services -The setup contains: +| Service | Image | Description | +|---------|-------|-------------| +| `web` | [nginx](https://hub.docker.com/_/nginx/) | Web server | +| `php` | [fewohbee-phpfpm](https://github.com/developeregrem/fewohbee-phpfpm) | PHP 8 FPM – clones and runs the app on first start | +| `cron` | [fewohbee-phpcli](https://github.com/developeregrem/fewohbee-phpfpm) | PHP CLI for scheduled tasks | +| `db` | [mariadb](https://hub.docker.com/_/mariadb) | Database | +| `redis` | [redis](https://hub.docker.com/_/redis) | In-memory cache | +| `acme` | [fewohbee-acme](https://github.com/developeregrem/fewohbee-acme) | SSL certificate management (Let's Encrypt or self-signed) | -- [nginx](https://hub.docker.com/_/nginx/) as web server or reverse proxy +## Configuration -- [mariadb](https://hub.docker.com/_/mariadb) as database management system +All settings are stored in a single `.env` file. Use `.env.dist` as the reference template. -- [PHP 8.5-fpm-alpine](https://hub.docker.com/_/php/) with [composer](https://hub.docker.com/_/composer) which [installs](https://github.com/developeregrem/fewohbee-phpfpm) the guesthouse administration tool when the container is started. +## Setup -- [redis](https://hub.docker.com/_/redis) as in-memory cache +### Option A – Setup container (recommended, all platforms) -- ACME for letsencrypt or self-signed certificates (with automatic renew) +Works on Linux, macOS and Windows — requires only Docker. -## Installation +```sh +# Clone the repository first +git clone https://github.com/developeregrem/fewohbee-dockerized.git +cd fewohbee-dockerized -Clone the master branch of the repository. +# Linux / macOS +docker run --rm -it -v $(pwd):/config developeregrem/fewohbee-setup - ```` - cd /opt - git clone https://github.com/developeregrem/fewohbee-dockerized.git - cd fewohbee-dockerized - ```` - -Run the interactive installtion script to generate the configuration file and setup the application. +# Windows PowerShell +docker run --rm -it -v ${PWD}:/config developeregrem/fewohbee-setup +``` - ```` - chmod +x install.sh - ./install.sh - ```` +The container asks a few questions (hostname, SSL mode, language), generates passwords and writes `.env`. -## Usage +### Option B – install.sh (Linux only) -Please refer to the documentation in the Wiki: [https://github.com/developeregrem/fewohbee/wiki/Docker-Setup](https://github.com/developeregrem/fewohbee/wiki/Docker-Setup) +A Bash script that additionally sets up optional cron jobs for database backups and automatic updates: + +```sh +git clone https://github.com/developeregrem/fewohbee-dockerized.git +cd fewohbee-dockerized +chmod +x install.sh +sudo ./install.sh +``` + +## Starting the application + +### Standard mode (with SSL) + +For servers with direct internet access. Manages SSL certificates automatically via the `acme` container (self-signed or Let's Encrypt). + +```sh +docker compose up -d +``` + +### Reverse proxy mode (no internal SSL) + +For deployments behind an external reverse proxy (Traefik, Nginx Proxy Manager, Caddy, etc.) that handles SSL termination. No `acme` container — the web container serves plain HTTP. + +Set `COMPOSE_FILE=docker-compose.no-ssl.yml` in `.env` (done automatically by the setup scripts when choosing `reverse-proxy`) and then: + +```sh +docker compose up -d +``` + +Configure the exposed HTTP port via `LISTEN_PORT` in `.env` (default: `80`). + +## First-run initialisation + +After starting the stack, the PHP container clones the app and installs dependencies (~2 minutes). Monitor progress: + +```sh +docker compose logs -f php +``` + +Once `ready to handle connections` appears, run once to create the first admin user: + +```sh +docker compose exec --user www-data php /bin/sh -c "php fewohbee/bin/console app:first-run" +``` + +## Updates + +```sh +chmod +x update-docker.sh +./update-docker.sh +``` + +The script pulls new images, restarts the stack and automatically syncs any new environment variables into `.env` and both compose files. New variables should be reviewed and adjusted after the update. + +## Documentation + +Full setup and configuration documentation: +[https://github.com/developeregrem/fewohbee/wiki/Docker-Setup](https://github.com/developeregrem/fewohbee/wiki/Docker-Setup) diff --git a/conf/db/create-backup-user.sh b/conf/db/create-backup-user.sh new file mode 100644 index 0000000..887fb62 --- /dev/null +++ b/conf/db/create-backup-user.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Creates the database backup user on first database initialization. +# Runs automatically via /docker-entrypoint-initdb.d/ on first start. +mariadb -u root -p"${MARIADB_ROOT_PASSWORD}" -e "GRANT LOCK TABLES, SELECT ON *.* TO '${MYSQL_BACKUP_USER}'@'%' IDENTIFIED BY '${MYSQL_BACKUP_PASSWORD}';" diff --git a/conf/nginx/Dockerfile b/conf/nginx/Dockerfile new file mode 100644 index 0000000..489a4d3 --- /dev/null +++ b/conf/nginx/Dockerfile @@ -0,0 +1,10 @@ +FROM nginx:mainline-alpine + +COPY . /etc/nginx/conf.d/ + +# Remove the default nginx config (replaced by site.conf) and the runtime-generated file +RUN rm -f /etc/nginx/conf.d/default.conf \ + /etc/nginx/conf.d/server_name.active \ + && chmod +x /etc/nginx/conf.d/docker-entrypoint.sh + +ENTRYPOINT ["/bin/sh", "/etc/nginx/conf.d/docker-entrypoint.sh"] diff --git a/conf/nginx/docker-entrypoint.sh b/conf/nginx/docker-entrypoint.sh new file mode 100644 index 0000000..ddcb147 --- /dev/null +++ b/conf/nginx/docker-entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/sh +set -e + +# Select site config based on SSL mode +if [ "${REVERSE_PROXY:-false}" = "true" ]; then + cp /etc/nginx/conf.d/site.conf.no-ssl /etc/nginx/conf.d/site.conf +fi + +# Generate the active server_name config from template +envsubst < /etc/nginx/conf.d/templates/server_name.template > /etc/nginx/conf.d/server_name.active + +if [ "${REVERSE_PROXY:-false}" != "true" ]; then + # Wait for SSL certificates to be provided by the acme container. + # On first start the certs volume is empty; acme writes the files shortly after launch. + echo "Waiting for SSL certificates ..." + while [ ! -f "/certs/fullchain.pem" ] || [ ! -f "/certs/privkey.pem" ] || [ ! -f "/certs/dhparams.pem" ]; do + sleep 2 + done + echo "Certificates found, starting nginx." +fi + +# Start nginx in the background so we can watch for cert changes +nginx -g 'daemon off;' & +NGINX_PID=$! + +if [ "${REVERSE_PROXY:-false}" != "true" ]; then + # Record the initial cert fingerprint + CERT_HASH=$(md5sum /certs/fullchain.pem | cut -d' ' -f1) + + # Watch for certificate renewal every 60 seconds and reload nginx when changed. + # This replaces the previous approach of restarting the container via the Docker socket. + while kill -0 "$NGINX_PID" 2>/dev/null; do + sleep 60 + NEW_HASH=$(md5sum /certs/fullchain.pem 2>/dev/null | cut -d' ' -f1) + if [ -n "$NEW_HASH" ] && [ "$NEW_HASH" != "$CERT_HASH" ]; then + CERT_HASH="$NEW_HASH" + echo "Certificate changed, reloading nginx ..." + nginx -s reload 2>/dev/null || true + fi + done +else + # In reverse proxy mode just wait for nginx to exit + while kill -0 "$NGINX_PID" 2>/dev/null; do + sleep 60 + done +fi + +wait "$NGINX_PID" diff --git a/conf/nginx/site-enabled-https/01_fewohbee.snippet b/conf/nginx/site-enabled-https/01_fewohbee.snippet index a59be25..ab584fb 100644 --- a/conf/nginx/site-enabled-https/01_fewohbee.snippet +++ b/conf/nginx/site-enabled-https/01_fewohbee.snippet @@ -7,7 +7,9 @@ location / { location ~ ^/index\.php(/|$) { root /var/www/html/fewohbee/public; fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass php:9000; + resolver 127.0.0.11 valid=5s; + set $php_backend php:9000; + fastcgi_pass $php_backend; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; diff --git a/conf/nginx/site.conf.no-ssl b/conf/nginx/site.conf.no-ssl new file mode 100644 index 0000000..251aa6d --- /dev/null +++ b/conf/nginx/site.conf.no-ssl @@ -0,0 +1,27 @@ +server_tokens off; + +server { + listen 80; + listen [::]:80; + index index.html index.php; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + + root /var/www/html/fewohbee/public; + + include /etc/nginx/conf.d/server_name.active; + + include /etc/nginx/conf.d/snippets/header.snippet; + + # app configs (PHP/FastCGI — reused from HTTPS setup) + include /etc/nginx/conf.d/site-enabled-https/*; +} + +server { + # this vhost is for internal connections only (e.g. mpdf fetches images from here, see env WEB_HOST) + listen 8080; + + root /var/www/html/fewohbee/public; + + include /etc/nginx/conf.d/server_name.active; +} diff --git a/conf/php/conf.ini b/conf/php/conf.ini deleted file mode 100644 index cdac5a9..0000000 --- a/conf/php/conf.ini +++ /dev/null @@ -1,19 +0,0 @@ -date.timezone=${TZ} - -opcache.enable=1 -opcache.enable_cli=1 -opcache.interned_strings_buffer=8 -opcache.max_accelerated_files=10000 -opcache.memory_consumption=128 -opcache.save_comments=1 -opcache.revalidate_freq=1 -opcache.jit_buffer_size=100M - -session.save_handler = redis -session.save_path = "tcp://redis:6379" - -expose_php = Off - -display_errors = 0 -error_reporting = E_ALL -log_errors = On diff --git a/cron.d/cli/www-data b/cron.d/cli/www-data deleted file mode 100644 index 926cc5e..0000000 --- a/cron.d/cli/www-data +++ /dev/null @@ -1,4 +0,0 @@ -SHELL=/bin/sh -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - -0 * * * * cd /var/www/html/fewohbee && /usr/local/bin/php bin/console calendar:import:sync --force diff --git a/docker-compose.no-ssl.yml b/docker-compose.no-ssl.yml new file mode 100644 index 0000000..075f622 --- /dev/null +++ b/docker-compose.no-ssl.yml @@ -0,0 +1,128 @@ +# Reverse-proxy mode: SSL is terminated by an external proxy (Traefik, Nginx, Caddy, etc.) +# The web container listens on plain HTTP on LISTEN_PORT (default: 80). +# No acme container — certificate management is handled by the external proxy. +# +# Usage: docker compose -f docker-compose.no-ssl.yml up -d + +services: + web: + build: ./conf/nginx + ports: + - ${LISTEN_PORT:-80}:80 + volumes: + - feb-data:/var/www/html:cached + environment: + - HOST_NAME=${HOST_NAME} + - REVERSE_PROXY=true + networks: + - internal-network + restart: always + + php: + image: developeregrem/fewohbee-phpfpm:latest + depends_on: + db: + condition: service_healthy + volumes: + - feb-data:/var/www/html:cached + networks: + - internal-network + environment: + - TZ=${TZ} + - DB_SERVER_VERSION=12.2.2-MariaDB + - LOCALE=${LOCALE} + - FEWOHBEE_VERSION=${FEWOHBEE_VERSION} + - APP_ENV=${APP_ENV} + - APP_SECRET=${APP_SECRET} + - REDIS_IDX=${REDIS_IDX} + - REDIS_HOST=${REDIS_HOST} + - USE_PASSWORD_BLACKLIST=${USE_PASSWORD_BLACKLIST} + - DATABASE_URL=${DATABASE_URL} + - MAILER_DSN=${MAILER_DSN} + - FROM_MAIL=${FROM_MAIL} + - FROM_NAME=${FROM_NAME} + - RETURN_PATH=${RETURN_PATH} + - MAIL_COPY=${MAIL_COPY} + - CUSTOMER_SALUTATIONS=${CUSTOMER_SALUTATIONS} + - RELYING_PARTY_ID=${RELYING_PARTY_ID} + - RELYING_PARTY_NAME=${RELYING_PARTY_NAME} + - PASSKEY_ENABLED=${PASSKEY_ENABLED} + - WEB_HOST=${WEB_HOST} + - INVOICE_FILENAME_PATTERN=${INVOICE_FILENAME_PATTERN} + # new-vars-marker + restart: always + + cron: + image: developeregrem/fewohbee-phpcli:latest + depends_on: + db: + condition: service_healthy + volumes: + - feb-data:/var/www/html:cached + networks: + - internal-network + environment: + - TZ=${TZ} + - DB_SERVER_VERSION=12.2.2-MariaDB + - LOCALE=${LOCALE} + - FEWOHBEE_VERSION=${FEWOHBEE_VERSION} + - APP_ENV=${APP_ENV} + - APP_SECRET=${APP_SECRET} + - REDIS_IDX=${REDIS_IDX} + - REDIS_HOST=${REDIS_HOST} + - USE_PASSWORD_BLACKLIST=${USE_PASSWORD_BLACKLIST} + - DATABASE_URL=${DATABASE_URL} + - MAILER_DSN=${MAILER_DSN} + - FROM_MAIL=${FROM_MAIL} + - FROM_NAME=${FROM_NAME} + - RETURN_PATH=${RETURN_PATH} + - MAIL_COPY=${MAIL_COPY} + - CUSTOMER_SALUTATIONS=${CUSTOMER_SALUTATIONS} + - RELYING_PARTY_ID=${RELYING_PARTY_ID} + - RELYING_PARTY_NAME=${RELYING_PARTY_NAME} + - PASSKEY_ENABLED=${PASSKEY_ENABLED} + - WEB_HOST=${WEB_HOST} + - INVOICE_FILENAME_PATTERN=${INVOICE_FILENAME_PATTERN} + # new-vars-marker + restart: always + + db: + image: mariadb:12.2 + restart: always + volumes: + - db-vol:/var/lib/mysql:cached + - db-backup-vol:/dbbackup:cached + - ./conf/db/create-backup-user.sh:/docker-entrypoint-initdb.d/create-backup-user.sh:ro + environment: + MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} + MARIADB_USER: ${MARIADB_USER} + MARIADB_PASSWORD: ${MARIADB_PASSWORD} + MARIADB_DATABASE: ${MARIADB_DATABASE} + MYSQL_BACKUP_PASSWORD: ${MYSQL_BACKUP_PASSWORD} + MYSQL_BACKUP_USER: ${MYSQL_BACKUP_USER} + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 30s + interval: 10s + timeout: 5s + retries: 5 + networks: + - internal-network + + redis: + image: redis:alpine + restart: always + volumes: + - redis-vol:/data/ + networks: + - internal-network + +networks: + internal-network: + driver: bridge + +volumes: + redis-vol: + db-vol: + feb-data: + db-backup-vol: diff --git a/docker-compose.yml b/docker-compose.yml index 1846b4f..f2ff4eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,88 +1,93 @@ services: web: - image: nginx:mainline-alpine + build: ./conf/nginx ports: - 80:80 - 443:443 volumes: - feb-data:/var/www/html:cached - - ./conf/nginx/:/etc/nginx/conf.d/ - certs-vol:/certs:ro environment: - HOST_NAME=${HOST_NAME} - command: /bin/sh -c "envsubst < /etc/nginx/conf.d/templates/server_name.template > /etc/nginx/conf.d/server_name.active && nginx -g 'daemon off;'" networks: - internal-network restart: always php: image: developeregrem/fewohbee-phpfpm:latest + depends_on: + db: + condition: service_healthy volumes: - feb-data:/var/www/html:cached - - ./conf/php/conf.ini:/usr/local/etc/php/conf.d/conf.ini - - ${WWW_ROOT}:/var/www/data:cached networks: - internal-network environment: - TZ=${TZ} + - DB_SERVER_VERSION=12.2.2-MariaDB - LOCALE=${LOCALE} - - FEWOHBEE_VERSION=${FEWOHBEE_VERSION:-latest} + - FEWOHBEE_VERSION=${FEWOHBEE_VERSION} - APP_ENV=${APP_ENV} - APP_SECRET=${APP_SECRET} + - REDIS_IDX=${REDIS_IDX} + - REDIS_HOST=${REDIS_HOST} + - USE_PASSWORD_BLACKLIST=${USE_PASSWORD_BLACKLIST} - DATABASE_URL=${DATABASE_URL} + - MAILER_DSN=${MAILER_DSN} - FROM_MAIL=${FROM_MAIL} - FROM_NAME=${FROM_NAME} - RETURN_PATH=${RETURN_PATH} - - MAILER_DSN=${MAILER_DSN:-null://localhost} - - DB_SERVER_VERSION=12.1.2-MariaDB - - WEB_HOST=${WEB_HOST} - - REDIS_HOST=${REDIS_HOST} - - REDIS_IDX=${REDIS_IDX} - - USE_PASSWORD_BLACKLIST=${USE_PASSWORD_BLACKLIST} + - MAIL_COPY=${MAIL_COPY} - CUSTOMER_SALUTATIONS=${CUSTOMER_SALUTATIONS} - - INVOICE_FILENAME_PATTERN=${INVOICE_FILENAME_PATTERN} - RELYING_PARTY_ID=${RELYING_PARTY_ID} - RELYING_PARTY_NAME=${RELYING_PARTY_NAME} - PASSKEY_ENABLED=${PASSKEY_ENABLED} + - WEB_HOST=${WEB_HOST} + - INVOICE_FILENAME_PATTERN=${INVOICE_FILENAME_PATTERN} + # new-vars-marker restart: always - + cron: image: developeregrem/fewohbee-phpcli:latest + depends_on: + db: + condition: service_healthy volumes: - feb-data:/var/www/html:cached - - ./conf/php/conf.ini:/usr/local/etc/php/conf.d/conf.ini - - ./cron.d/cli:/opt/cron:ro networks: - internal-network environment: - TZ=${TZ} + - DB_SERVER_VERSION=12.2.2-MariaDB - LOCALE=${LOCALE} - - FEWOHBEE_VERSION=${FEWOHBEE_VERSION:-latest} + - FEWOHBEE_VERSION=${FEWOHBEE_VERSION} - APP_ENV=${APP_ENV} - APP_SECRET=${APP_SECRET} + - REDIS_IDX=${REDIS_IDX} + - REDIS_HOST=${REDIS_HOST} + - USE_PASSWORD_BLACKLIST=${USE_PASSWORD_BLACKLIST} - DATABASE_URL=${DATABASE_URL} + - MAILER_DSN=${MAILER_DSN} - FROM_MAIL=${FROM_MAIL} - FROM_NAME=${FROM_NAME} - RETURN_PATH=${RETURN_PATH} - - MAILER_DSN=${MAILER_DSN:-null://localhost} - - DB_SERVER_VERSION=12.1.2-MariaDB - - WEB_HOST=${WEB_HOST} - - REDIS_HOST=${REDIS_HOST} - - REDIS_IDX=${REDIS_IDX} - - USE_PASSWORD_BLACKLIST=${USE_PASSWORD_BLACKLIST} + - MAIL_COPY=${MAIL_COPY} - CUSTOMER_SALUTATIONS=${CUSTOMER_SALUTATIONS} - RELYING_PARTY_ID=${RELYING_PARTY_ID} - RELYING_PARTY_NAME=${RELYING_PARTY_NAME} - PASSKEY_ENABLED=${PASSKEY_ENABLED} + - WEB_HOST=${WEB_HOST} + - INVOICE_FILENAME_PATTERN=${INVOICE_FILENAME_PATTERN} + # new-vars-marker restart: always db: - image: mariadb:12.1 + image: mariadb:12.2 restart: always volumes: - db-vol:/var/lib/mysql:cached - - ${MYSQL_BACKUP_FOLDER}:/dbbackup:cached - - ./data/db:/db + - db-backup-vol:/dbbackup:cached + - ./conf/db/create-backup-user.sh:/docker-entrypoint-initdb.d/create-backup-user.sh:ro environment: MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} MARIADB_USER: ${MARIADB_USER} @@ -90,9 +95,15 @@ services: MARIADB_DATABASE: ${MARIADB_DATABASE} MYSQL_BACKUP_PASSWORD: ${MYSQL_BACKUP_PASSWORD} MYSQL_BACKUP_USER: ${MYSQL_BACKUP_USER} + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 30s + interval: 10s + timeout: 5s + retries: 5 networks: - internal-network - + redis: image: redis:alpine restart: always @@ -100,36 +111,33 @@ services: - redis-vol:/data/ networks: - internal-network - + acme: image: developeregrem/fewohbee-acme:latest volumes: - certs-vol:/certs - - /var/run/docker.sock:/var/run/docker.sock - feb-data:/var/www:cached restart: always - entrypoint: "" environment: - - HOST_NAME=${HOST_NAME} # used for self signed certificate + - HOST_NAME=${HOST_NAME} - LETSENCRYPT_DOMAINS=${LETSENCRYPT_DOMAINS} - EMAIL=${EMAIL} - SELF_SIGNED=${SELF_SIGNED} - LETSENCRYPT=${LETSENCRYPT} - TZ=${TZ} - - DOCKER_API_VERSION=${DOCKER_API_VERSION} - DYNDNS_PROVIDER=${DYNDNS_PROVIDER} - DEDYN_TOKEN=${DEDYN_TOKEN} - DEDYN_NAME=${DEDYN_NAME} networks: - internal-network - + networks: internal-network: driver: bridge - + volumes: redis-vol: certs-vol: db-vol: feb-data: - + db-backup-vol: diff --git a/install.sh b/install.sh index fb108ce..f76791e 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ createCron() { return 1 fi targetCron="/etc/cron.d/$1" - ln -s $PWD/cron.d/$1 $targetCron + ln -s $PWD/cron.d/$1 $targetCron if [ $? -ne 0 ] then echo "Could not create symlink $targetCron. Do you have the permission to write there?" @@ -55,7 +55,7 @@ checkRequirements echo "This script will guide you through the installation of the tool." -# use env.dist as template and replace specific values during script execution +# use .env.dist as template and replace specific values during script execution umask 0177 envTemplate=.env.dist envTmp=.env.tmp @@ -71,16 +71,15 @@ pveHost="${pveHost:-${pveHostDefault}}" $(sed "s/HOST_NAME=fewohbee/HOST_NAME=$pveHost/" $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) $(sed "s/RELYING_PARTY_ID=example.com/RELYING_PARTY_ID=$pveHost/" $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) -########## setup certificate self-signed or letsencrypt ########## +########## setup certificate ########## sslDefault="self-signed" ssl="" -while ! [[ "$ssl" =~ ^(self-signed|letsencrypt)$ ]] +while ! [[ "$ssl" =~ ^(self-signed|letsencrypt|reverse-proxy)$ ]] do - read -p "SSL Certificate: Using self-signed or letsencrypt? [$sslDefault]:" ssl + read -p "SSL Certificate: Using self-signed, letsencrypt or reverse-proxy? [$sslDefault]:" ssl ssl="${ssl:-${sslDefault}}" done -# default is self-signed if [ "$ssl" == "letsencrypt" ] then # ask for email for letsencrypt @@ -108,6 +107,13 @@ then $(sed 's/EMAIL=""/EMAIL='"$leMail"'/g' $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) fi +# reverse-proxy: SSL is terminated externally — disable internal SSL and switch compose file +if [ "$ssl" == "reverse-proxy" ] +then + $(sed 's@SELF_SIGNED=true@SELF_SIGNED=false@g' $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) + $(sed 's@COMPOSE_FILE=docker-compose.yml@COMPOSE_FILE=docker-compose.no-ssl.yml@g' $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) +fi + ########## setup cron ########## cronDefault="yes" cronDB="" @@ -120,9 +126,9 @@ then createCron "backup_mysql_docker" if [ $? -eq 0 ] then - echo "Backups will be stored in ../dbbackup." + echo "Backups will be stored in the db-backup-vol Docker volume." fi - chmod +x backup-db.sh + chmod +x backup-db.sh fi read -p "Enable automatic updates of docker images? (yes/no) [$cronDefault]:" cronDocker @@ -136,7 +142,7 @@ fi ########## setup symfony env ########## pveEnvDefault="prod" pveEnv="" -while ! [[ "$pveEnv" =~ ^(prod|dev)$ ]] +while ! [[ "$pveEnv" =~ ^(prod|dev)$ ]] do read -p "Do you want to run the tool in productive mode oder development mode (prod/dev) [$pveEnvDefault]:" pveEnv pveEnv="${pveEnv:-${pveEnvDefault}}" @@ -151,13 +157,14 @@ fi ### select language ### pveLangDefault="de" pveLang="" -while ! [[ "$pveLang" =~ ^(de|en)$ ]] +while ! [[ "$pveLang" =~ ^(de|en)$ ]] do read -p "Please choose the language of the tool (de/en) [$pveLangDefault]:" pveLang pveLang="${pveLang:-${pveLangDefault}}" done $(sed 's@APP_ENV=prod@APP_ENV='"$pveEnv"'@g' $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) +$(sed "s@LOCALE=de@LOCALE=$pveLang@g" $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) echo "Setting up $pveEnv environment." echo "Generating secrets and passwords." @@ -170,9 +177,8 @@ $(sed 's@MARIADB_ROOT_PASSWORD=@MARIADB_ROOT_PASSWORD='"$mariadbRootPw"'@g' $(sed 's@MARIADB_PASSWORD=@MARIADB_PASSWORD='"$mariadbPw"'@g' $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) $(sed "s@MYSQL_BACKUP_PASSWORD=@MYSQL_BACKUP_PASSWORD=$mysqlBackupPw@g" $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) $(sed "s@APP_SECRET=@APP_SECRET=$appSecret@g" $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) -$(sed "s@LOCALE=de@LOCALE=$pveLang@g" $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) -# replace db password in db string +# replace db password in DATABASE_URL $(sed "s@db_password@$mariadbPw@" $envTmp > $envTmp.tmp && mv $envTmp.tmp $envTmp) mv $envTmp $envEnd @@ -187,17 +193,12 @@ then exit 1 fi -########## ssl setup ########## -echo "Initiating certificate creation ..." -sleep 3 -$dockerComposeBin exec acme /bin/sh -c "./run.sh" - ########## application setup ########## echo "Setting up application ..." echo "Pulling app dependencies and setting up the database (this will take some time)." # this check depends on the script entrypoint.sh from fewohbee-phpfpm image until [ "`$dockerComposeBin exec -T php /bin/sh -c 'cat /firstrun'`" == "1" ] -do +do echo "still waiting ..." sleep 10 done @@ -211,25 +212,13 @@ $dockerComposeBin exec db /bin/sh -c "mariadb -p$mariadbRootPw -uroot -e '$dbQue ########## init tool ########## $dockerComposeBin exec --user www-data php /bin/sh -c "php fewohbee/bin/console app:first-run" -########## load test data ########## -## always load templates -$dockerComposeBin exec --user www-data php /bin/sh -c "php fewohbee/bin/console doctrine:fixtures:load --append --group templates" -testDataDefault="no" -testData="" -while ! [[ "$testData" =~ ^(yes|no|y|n)$ ]] -do - read -p "Do you want to load some initial test data into the application? (yes/no) [$testDataDefault]:" testData - testData="${testData:-${testDataDefault}}" -done - -# default is self-signed -if [ "$testData" == "yes" ] +echo "done" +if [ "$ssl" == "reverse-proxy" ] then - $dockerComposeBin exec --user www-data php /bin/sh -c "php fewohbee/bin/console doctrine:fixtures:load --append --group settings --group customer --group reservation --group invoices" + echo "You can now open a browser and visit http://$pveHost (via reverse proxy)." +else + echo "You can now open a browser and visit https://$pveHost." fi - -echo "done" -echo "You can now open a browser and visit https://$pveHost." echo "If you want to use the conversation feature please modify the section in the .env file accordingly." echo " > see https://github.com/developeregrem/fewohbee/wiki/Konfiguration#e-mails" echo "To use the city lookup feature please refer to: https://github.com/developeregrem/fewohbee/wiki/City-Lookup" diff --git a/update-docker.sh b/update-docker.sh index 6f48477..f839f2f 100755 --- a/update-docker.sh +++ b/update-docker.sh @@ -3,8 +3,94 @@ cd "$(dirname "$0")" dockerBin=$(/usr/bin/which docker) + +# Pull latest changes to docker-compose.yml and configuration files +#git pull + +# Pull and build new images $dockerBin compose pull $dockerBin compose build --force-rm --pull + +# Start containers. The php entrypoint will clone/update fewohbee via git. $dockerBin compose stop $dockerBin compose up --force-recreate -d + +# Wait for fewohbee to finish setup (git clone/pull + composer + migrations) +echo "Waiting for fewohbee to finish setup ..." +until [ "$($dockerBin compose exec -T php /bin/sh -c 'cat /firstrun' 2>/dev/null)" == "1" ]; do + echo " still waiting ..." + sleep 10 +done + +# Sync new environment variables from the now-running container into .env +echo "Checking for new environment variables ..." +containerEnvDist=$($dockerBin compose exec --user www-data -T php /bin/sh -c "cat fewohbee/.env.dist" 2>/dev/null) + +if [ $? -ne 0 ] || [ -z "$containerEnvDist" ]; then + echo "Warning: Could not read .env.dist from container. Skipping env sync." +else + addedVars=0 + commentBuffer="" + + while IFS= read -r line; do + # Accumulate comments and empty lines to carry them along with their variable + if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "${line// }" ]]; then + [ -n "$commentBuffer" ] && commentBuffer+=$'\n' + commentBuffer+="$line" + continue + fi + + # Extract variable name (everything before the first =) + varName="${line%%=*}" + if [[ -z "$varName" ]]; then + commentBuffer="" + continue + fi + + # DB_SERVER_VERSION is hardcoded in docker-compose.yml, skip it + if [[ "$varName" == "DB_SERVER_VERSION" ]]; then + commentBuffer="" + continue + fi + + # Add if not already present in .env + if ! grep -q "^${varName}=" .env; then + if [ $addedVars -eq 0 ]; then + printf "\n# Variables added by update-docker.sh on %s\n" "$(date '+%Y-%m-%d')" >> .env + fi + # Write accumulated comments first, then the variable + if [ -n "$commentBuffer" ]; then + printf "\n%s\n" "$commentBuffer" >> .env + fi + printf "%s\n" "$line" >> .env + + # Also add the variable to the environment: sections in both compose files + # (inserted before the # new-vars-marker comment, which appears in php and cron services) + for composeFile in docker-compose.yml docker-compose.no-ssl.yml; do + if [ -f "$composeFile" ] && grep -q "# new-vars-marker" "$composeFile"; then + tmpfile=$(mktemp) + awk -v varline=" - ${varName}=\${${varName}}" \ + '/# new-vars-marker/ { print varline } { print }' \ + "$composeFile" > "$tmpfile" && mv "$tmpfile" "$composeFile" + fi + done + + echo " Added: $varName" + addedVars=$((addedVars + 1)) + fi + + # Reset comment buffer after each variable (whether added or already present) + commentBuffer="" + done <<< "$containerEnvDist" + + if [ $addedVars -gt 0 ]; then + echo "$addedVars new variable(s) added to .env and docker-compose.yml / docker-compose.no-ssl.yml." + echo "Please review the new variables in .env and adjust values if needed." + echo "Restarting php and cron containers to apply new environment variables ..." + $dockerBin compose up --force-recreate -d php cron + else + echo "No new environment variables found." + fi +fi + docker image prune -f