├── .gitattributes ├── .github ├── renovate.json └── workflows │ ├── stale.yml │ └── update.yml ├── .gitignore ├── LICENSE ├── README.md ├── dev ├── .gitignore ├── Patches.md ├── baker.py ├── build_test_version.py └── requirements.txt ├── docker-compose.yml └── docker ├── .htaccess ├── Dockerfile ├── apache ├── webtrees-redir.conf ├── webtrees-ssl.conf └── webtrees.conf ├── docker-entrypoint.py ├── docker-healthcheck.sh └── patches ├── UpgradeService1.patch └── UpgradeService2.patch /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://code.visualstudio.com/docs/remote/troubleshooting#_resolving-git-line-ending-issues-in-containers-resulting-in-many-modified-files 2 | * text=auto eol=lf 3 | *.{cmd,[cC][mM][dD]} text eol=crlf 4 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>nathanvaughn/renovate-config"] 4 | } -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: write 13 | issues: write 14 | pull-requests: write 15 | 16 | steps: 17 | - uses: actions/stale@v9 18 | with: 19 | stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days." 20 | days-before-stale: 60 21 | days-before-close: 7 22 | exempt-issue-labels: notstale 23 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: Check and Push Updates 2 | 3 | on: 4 | schedule: 5 | - cron: "0 12 * * *" 6 | workflow_dispatch: 7 | inputs: 8 | forced: 9 | type: string 10 | required: false 11 | description: "Space-seperated version numbers to force update of" 12 | 13 | jobs: 14 | bake: 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: read 19 | 20 | steps: 21 | - name: Checkout Code 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.13" 28 | cache: pip 29 | 30 | - name: Install Requirements 31 | run: | 32 | python -m pip install pip wheel --upgrade 33 | python -m pip install -r dev/requirements.txt 34 | 35 | - name: Get Versions 36 | id: script 37 | run: python3 dev/baker.py --forced ${{ inputs.forced }} 38 | env: 39 | GITHUB_TOKEN: ${{ github.token }} 40 | 41 | outputs: 42 | matrixes: ${{ steps.script.outputs.matrixes }} 43 | 44 | build: 45 | needs: bake 46 | if: ${{ fromJSON(needs.bake.outputs.matrixes).builder.include[0] != null }} 47 | strategy: 48 | matrix: ${{ fromJSON(needs.bake.outputs.matrixes).builder }} 49 | 50 | permissions: 51 | contents: read 52 | packages: write 53 | 54 | uses: NathanVaughn/reusable-actions/.github/workflows/docker-build-push.yml@main 55 | with: 56 | attest_id: ${{ matrix.attest_id }} 57 | platform: ${{ matrix.platform }} 58 | tags: ${{ matrix.tags }} 59 | context: docker 60 | dockerfile: docker/Dockerfile 61 | buildargs: | 62 | WEBTREES_VERSION=${{ matrix.webtrees_version }} 63 | PHP_VERSION=${{ matrix.php_version }} 64 | PATCH_VERSION=${{ matrix.patch_version }} 65 | secrets: 66 | dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} 67 | dockerhub_password: ${{ secrets.DOCKERHUB_PASSWORD }} 68 | 69 | attest: 70 | needs: 71 | - bake 72 | - build 73 | strategy: 74 | matrix: ${{ fromJSON(needs.bake.outputs.matrixes).attester }} 75 | 76 | permissions: 77 | id-token: write 78 | contents: read 79 | attestations: write 80 | packages: write 81 | 82 | uses: NathanVaughn/reusable-actions/.github/workflows/docker-attest.yml@main 83 | with: 84 | name: ${{ matrix.name }} 85 | attest_id: ${{ matrix.attest_id }} 86 | # secrets: 87 | # dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} 88 | # dockerhub_password: ${{ secrets.DOCKERHUB_PASSWORD }} 89 | 90 | update-dockerhub: 91 | needs: build 92 | runs-on: ubuntu-latest 93 | 94 | permissions: 95 | contents: read 96 | 97 | steps: 98 | - name: Checkout Code 99 | uses: actions/checkout@v4 100 | 101 | - name: Update DockerHub README 102 | uses: christian-korneck/update-container-description-action@v1 103 | env: 104 | DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }} 105 | DOCKER_PASS: ${{ secrets.DOCKERHUB_PASSWORD }} 106 | with: 107 | destination_container_repo: ${{ secrets.DOCKERHUB_USERNAME }}/webtrees 108 | provider: dockerhub 109 | short_description: ${{ github.event.repository.description }} 110 | 111 | create-releases: 112 | needs: 113 | - bake 114 | - build 115 | if: ${{ fromJSON(needs.bake.outputs.matrixes).releaser.include[0] != null }} 116 | 117 | strategy: 118 | matrix: ${{ fromJSON(needs.bake.outputs.matrixes).releaser }} 119 | 120 | permissions: 121 | contents: write 122 | 123 | uses: NathanVaughn/reusable-actions/.github/workflows/create-release.yml@main 124 | with: 125 | tag: ${{ matrix.tag }} 126 | body: ${{ matrix.body }} 127 | prerelease: ${{ matrix.prerelease }} 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nathan Vaughn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Image for [webtrees](https://webtrees.net/) 2 | 3 | [![](https://github.com/NathanVaughn/webtrees-docker/workflows/Check%20and%20Push%20Updates/badge.svg)](https://github.com/NathanVaughn/webtrees-docker) 4 | [![](https://img.shields.io/docker/v/nathanvaughn/webtrees)](https://hub.docker.com/r/nathanvaughn/webtrees) 5 | [![](https://img.shields.io/docker/image-size/nathanvaughn/webtrees)](https://hub.docker.com/r/nathanvaughn/webtrees) 6 | [![](https://img.shields.io/docker/pulls/nathanvaughn/webtrees)](https://hub.docker.com/r/nathanvaughn/webtrees) 7 | [![](https://img.shields.io/github/license/nathanvaughn/webtrees-docker)](https://github.com/NathanVaughn/webtrees-docker) 8 | 9 | This is a multi-architecture, up-to-date, Docker image for 10 | [webtrees](https://github.com/fisharebest/webtrees) served over HTTP or HTTPS. 11 | This can be put behind a reverse proxy such as CloudFlare or Traefik, or 12 | run standalone. 13 | 14 | ## Usage 15 | 16 | ### Quickstart 17 | 18 | If you want to jump right in, take a look at the provided 19 | [docker-compose.yml](https://github.com/NathanVaughn/webtrees-docker/blob/master/docker-compose.yml). 20 | 21 | ### Environment Variables 22 | 23 | There are many environment variables available to help automatically configure 24 | the container. For any environment variable you do not define, 25 | the default value will be used. 26 | 27 | > **🚨 WARNING 🚨** 28 | > These environment variables will be visible in the webtrees control panel 29 | > under "Server information". Either lock down the control panel 30 | > to administrators, or use the webtrees setup wizard. 31 | 32 | | Environment Variable | Required | Default | Notes | 33 | | -------------------------------------------------------------------------- | -------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 34 | | `PRETTY_URLS` | No | `False` | Setting this to any truthy value (`True`, `1`, `yes`) will enable [pretty URLs](https://webtrees.net/faq/urls/). This can be toggled at any time, however you must go through initial setup at least once first. | 35 | | `HTTPS` or `SSL` | No | `False` | Setting this to any truthy value (`True`, `1`, `yes`) will enable HTTPS. If `True`, you must also fill out `SSL_CERT_FILE` and `SSL_CERT_KEY_FILE` | 36 | | `HTTPS_REDIRECT` or `SSL_REDIRECT` | No | `False` | Setting this to any truthy value (`True`, `1`, `yes`) will enable a _permanent_ 301 redirect to HTTPS . Leaving this off will allow webtrees to be accessed over HTTP, but not automatically redirected to HTTPS. | 37 | | `SSL_CERT_FILE` | No | `/certs/webtrees.crt` | Certificate file to use for HTTPS. Can either be absolute, or relative to `/var/www/webtrees/data/`. | 38 | | `SSL_CERT_KEY_FILE` | No | `/certs/webtrees.key` | Certificate key file to use for HTTPS. Can either be absolute, or relative to `/var/www/webtrees/data/`. | 39 | | `LANG` | Yes | `en-us` | webtrees localization setting. This takes a locale code. List: | 40 | | `BASE_URL` | Yes | None | Base URL of the installation, with protocol. This needs to be in the form of `http://webtrees.example.com` | 41 | | `DB_TYPE` | Yes | `mysql` | Database server type. See [below](#database) for valid values. | 42 | | `DB_HOST` | Yes | None | Database server host. | 43 | | `DB_PORT` | Yes | `3306` | Database server port. | 44 | | `DB_USER` or `MYSQL_USER` or `MARIADB_USER` or `POSTGRES_USER` | Yes | `webtrees` | Database server username. | 45 | | `DB_PASS` or `MYSQL_PASSWORD` or `MARIADB_PASSWORD` or `POSTGRES_PASSWORD` | Yes | None | Database server password. | 46 | | `DB_NAME` or `MYSQL_DATABASE` or `MARIADB_DATABASE` or `POSTGRES_DB` | Yes | `webtrees` | Database name. | 47 | | `DB_PREFIX` | Yes | `wt_` | Prefix to give all tables in the database. Set this to a value of `""` to have no table prefix. | 48 | | `DB_KEY` | No | None | Key file used to verify the MySQL server. Only use with the `mysql` database driver. Relative to the `/var/www/webtrees/data/` directory. | 49 | | `DB_CERT` | No | None | Certificate file used to verify the MySQL server. Only use with the `mysql` database driver. Relative to the `/var/www/webtrees/data/` directory. | 50 | | `DB_CA` | No | None | Certificate authority file used to verify the MySQL server. Only use with the `mysql` database driver. Relative to the `/var/www/webtrees/data/` directory. | 51 | | `DB_VERIFY` | No | `False` | Whether to verify the MySQL server. Only use with the `mysql` database driver. If `True`, you must also fill out `DB_KEY`, `DB_CERT`, and `DB_CA`. | 52 | | `WT_USER` | Yes | None | First admin account username. Note, this is only used the first time the container is run, and the database is initialized. | 53 | | `WT_NAME` | Yes | None | First admin account full name. Note, this is only used the first time the container is run, and the database is initialized. | 54 | | `WT_PASS` | Yes | None | First admin account password. Note, this is only used the first time the container is run, and the database is initialized. | 55 | | `WT_EMAIL` | Yes | None | First admin account email. Note, this is only used the first time the container is run, and the database is initialized. | 56 | | `PHP_MEMORY_LIMIT` | No | `1024M` | PHP memory limit. See the [PHP documentation](https://www.php.net/manual/en/ini.core.php#ini.memory-limit) | 57 | | `PHP_MAX_EXECUTION_TIME` | No | `90` | PHP max execution time for a request in seconds. See the [PHP documentation](https://www.php.net/manual/en/info.configuration.php#ini.max-execution-time) | 58 | | `PHP_POST_MAX_SIZE` | No | `50M` | PHP POST request max size. See the [PHP documentation](https://www.php.net/manual/en/ini.core.php#ini.post-max-size) | 59 | | `PHP_UPLOAD_MAX_FILE_SIZE` | No | `50M` | PHP max uploaded file size. See the [PHP documentation](https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize) | 60 | | `PUID` | No | `33` | See [https://docs.linuxserver.io/general/understanding-puid-and-pgid/](https://docs.linuxserver.io/general/understanding-puid-and-pgid/) | 61 | | `PGID` | No | `33` | See [https://docs.linuxserver.io/general/understanding-puid-and-pgid/](https://docs.linuxserver.io/general/understanding-puid-and-pgid/) 62 | 63 | Additionally, you can add `_FILE` to the end of any environment variable name, 64 | and instead that will read the value in from the given filename. 65 | For example, setting `DB_PASS_FILE=/run/secrets/my_db_secret` will read the contents 66 | of that file into `DB_PASS`. 67 | 68 | If you don't want the container to be configured automatically 69 | (if you're migrating from an existing webtrees installation for example), simply leave 70 | the database (`DB_`) and webtrees (`WT_`) variables blank, and you can complete the 71 | [setup wizard](https://i.imgur.com/rw70cgW.png) like normal. 72 | 73 | ### Database 74 | 75 | webtrees [recommends](https://webtrees.net/install/requirements/) 76 | a MySQL (or compatible equivalent) database. 77 | You will need a separate container for this. 78 | 79 | - [MariaDB](https://hub.docker.com/_/mariadb) 80 | - [MySQL](https://hub.docker.com/_/mysql) 81 | 82 | PostgreSQL (`pgsql`) and SQLite (`sqlite`) are additionally both supported by 83 | webtrees and this image, but are [not recommended](https://github.com/fisharebest/webtrees/issues/5099#issuecomment-2581440755). 84 | This image does not support Microsoft SQL Server, in order to support multiple 85 | architectures. See issue: 86 | [microsoft/msphpsql#441](https://github.com/microsoft/msphpsql/issues/441#issuecomment-310237200) 87 | 88 | #### SQLite Values 89 | 90 | If you want to use a SQLite database, set the following values: 91 | 92 | - `DB_TYPE` to `sqlite` 93 | - `DB_NAME` to `desiredfilename`. Do not include any extension. 94 | 95 | #### PostgreSQL Values 96 | 97 | If you want to use a PostreSQL database, set the following values: 98 | 99 | - `DB_TYPE` to `pgsql` 100 | - `DB_PORT` to `5432` 101 | 102 | All other values are just like a MySQL database. 103 | 104 | ### Volumes 105 | 106 | The image mounts: 107 | 108 | - `/var/www/webtrees/data/` 109 | 110 | (media is stored in the `media` subfolder) 111 | 112 | If you want to add custom [themes or modules](https://webtrees.net/download/modules), 113 | you can also mount the `/var/www/webtrees/modules_v4/` directory. 114 | 115 | Example `docker-compose`: 116 | 117 | ```yml 118 | volumes: 119 | - app_data:/var/www/webtrees/data/ 120 | - app_themes:/var/www/webtrees/modules_v4/ 121 | --- 122 | volumes: 123 | app_data: 124 | driver: local 125 | app_themes: 126 | driver: local 127 | ``` 128 | 129 | See the link above for information about v1.7 webtrees. 130 | 131 | To install a custom theme or module, the process is generally as follows: 132 | 133 | ```bash 134 | docker exec -it webtrees_app_1 bash # connect to the running container 135 | cd /var/www/webtrees/modules_v4/ # move into the modules directory 136 | curl -L -o # download the file 137 | 138 | # if module is a .tar.gz file 139 | tar -xf # extract the tar archive https://xkcd.com/1168/ 140 | rm # remove the tar archive 141 | 142 | # if module is a .zip file 143 | apt update && apt install unzip # install the unzip package 144 | unzip # extract the zip file 145 | rm # remove the zip file 146 | 147 | exit # disconnect from the container 148 | ``` 149 | 150 | ### Network 151 | 152 | The image exposes port 80 and 443. 153 | 154 | Example `docker-compose`: 155 | 156 | ```yml 157 | ports: 158 | - 80:80 159 | - 443:443 160 | ``` 161 | 162 | If you have the HTTPS redirect enabled, you still need to expose port 80. 163 | If you're not using HTTPS at all, you don't need to expose port 443. 164 | 165 | ### ImageMagick 166 | 167 | `ImageMagick` is included in this image to speed up 168 | [thumbnail creation](https://webtrees.net/faq/thumbnails/). 169 | webtrees will automatically prefer it over `gd` with no configuration. 170 | 171 | ## Tags 172 | 173 | ### Specific Versions 174 | 175 | Each stable, legacy, beta, and alpha release version of webtrees 176 | produces a version-tagged build of the Docker container. 177 | 178 | Example: 179 | 180 | ```yml 181 | image: ghcr.io/nathanvaughn/webtrees:2.1.2 182 | ``` 183 | 184 | ### Latest 185 | 186 | Currently, the tags `latest`, `latest-alpha`, `latest-beta` and `latest-legacy` 187 | are available for the latest stable, alpha, beta and legacy versions of webtrees, 188 | respectively. 189 | 190 | Example: 191 | 192 | ```yml 193 | image: ghcr.io/nathanvaughn/webtrees:latest 194 | ``` 195 | 196 | > **Note** 197 | > Legacy versions of webtrees are no longer supported. 198 | 199 | ## Issues 200 | 201 | New releases of the Dockerfile are automatically generated from upstream 202 | webtrees versions. This means a human does not vette every release. While 203 | I try to stay on top of things, sometimes breaking issues do occur. If you 204 | have any, please feel free to fill out an 205 | [issue](https://github.com/NathanVaughn/webtrees-docker/issues). 206 | 207 | ## Reverse Proxy Issues 208 | 209 | webtrees does not like running behind a reverse proxy, and depending on your setup, 210 | you may need to adjust some database values manually. 211 | 212 | For example, if you are accessing webtrees via a reverse proxy serving content 213 | over HTTPS, but using this container with HTTP, you _might_ need to make the following 214 | changes in your database: 215 | 216 | ```sql 217 | mysql -u webtrees -p 218 | 219 | use webtrees; 220 | update wt_site_setting set setting_value='https://example.com/login' where setting_name='LOGIN_URL'; 221 | update wt_site_setting set setting_value='https://example.com/' where setting_name='SERVER_URL'; 222 | quit; 223 | ``` 224 | 225 | For more info, see [this](https://webtrees.net/admin/proxy/). 226 | 227 | ## Registry 228 | 229 | This image is available from 2 different registries. Choose whichever you want: 230 | 231 | - [docker.io/nathanvaughn/webtrees](https://hub.docker.com/r/nathanvaughn/webtrees) 232 | - [ghcr.io/nathanvaughn/webtrees](https://github.com/users/nathanvaughn/packages/container/package/webtrees) 233 | -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | .last_built_version -------------------------------------------------------------------------------- /dev/Patches.md: -------------------------------------------------------------------------------- 1 | To create a patch, download a release .zip file and extract. 2 | 3 | Slice out the contents of `fetchLatestVersion` leaving 4 | `return Site::getPreference('LATEST_WT_VERSION');` 5 | in webtrees\app\Services\UpgradeService.php and make a copy. 6 | 7 | Run 8 | 9 | ```powershell 10 | git diff .\UpgradeService.php .\UpgradeServicePatched.php > .\UpgradeService.patch 11 | ``` 12 | 13 | in that folder. 14 | 15 | Rename and copy into the repo. Update the dict in `versionchecker.py`. 16 | -------------------------------------------------------------------------------- /dev/baker.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import sys 5 | import urllib.request 6 | from typing import Dict, List, Optional 7 | 8 | from retry import retry 9 | 10 | WEBTREES_REPO = "fisharebest/webtrees" 11 | MY_REPO = os.getenv("GITHUB_REPOSITORY", default="nathanvaughn/webtrees-docker") 12 | ARCHITECTURES = ["linux/amd64", "linux/arm/v7", "linux/arm64"] 13 | BASE_IMAGES = [ 14 | "index.docker.io/nathanvaughn/webtrees", 15 | "ghcr.io/nathanvaughn/webtrees", 16 | ] 17 | 18 | # https://webtrees.net/install/ 19 | # PHP 8.4 is still broken with imagick. 20 | # See https://github.com/Imagick/imagick/issues/698 21 | WEBTREES_PHP = {"1.": "7.4", "2.0": "7.4", "2.1": "8.1", "2.2": "8.3"} 22 | WEBTREES_PATCH = {"2.1.18": "1", "2.1.19": "2", "default": "2"} 23 | 24 | # used to use 'name' of release, but this has started being blank 25 | VERSION_KEY = "tag_name" 26 | 27 | 28 | @retry(tries=5, delay=2, backoff=3) 29 | def get_latest_versions( 30 | repo: str, number: int = 5, check_assets: bool = False 31 | ) -> List[dict]: 32 | """ 33 | Get latest versions from a repository releases 34 | """ 35 | 36 | # build url 37 | url = f"https://api.github.com/repos/{repo}/releases" 38 | request = urllib.request.Request(url) 39 | 40 | if os.getenv("GITHUB_TOKEN"): 41 | request.add_header("Authorization", f'Bearer {os.environ["GITHUB_TOKEN"]}') 42 | 43 | # download data 44 | data = urllib.request.urlopen(request) 45 | # parse json 46 | json_data = json.loads(data.read().decode()) 47 | # only get the latest items 48 | latest_releases = json_data[:number] 49 | 50 | # skip releases with no assets 51 | if check_assets: 52 | for release in latest_releases: 53 | if not release["assets"]: 54 | latest_releases.remove(release) 55 | 56 | return latest_releases 57 | 58 | 59 | def get_tags(versions: List[str]) -> Dict[str, List[str]]: 60 | """ 61 | Create a list of tags for a given version number 62 | """ 63 | # dict of list of tags to return 64 | versions_tags = {} 65 | 66 | # all tags seen, so we don't duplicate 67 | tags_seen = set() 68 | 69 | # sort descending to work from newest to oldest 70 | for version in sorted(versions, reverse=True): 71 | tag_list = [version] 72 | 73 | if version.startswith("1."): 74 | tag = "latest-1" 75 | elif version.startswith("2.0"): 76 | tag = "latest-2.0" 77 | elif version.startswith("2.1"): 78 | tag = "latest-2.1" 79 | else: 80 | tag = "latest" 81 | 82 | if "alpha" in version: 83 | tag += "-alpha" 84 | elif "beta" in version: 85 | tag += "-beta" 86 | 87 | # check against our list of all tags seen to make sure we don't have duplicates 88 | if tag not in tags_seen: 89 | tag_list.append(tag) 90 | tags_seen.add(tag) 91 | 92 | versions_tags[version] = [ 93 | f"{base_image}:{t}" for t in tag_list for base_image in BASE_IMAGES 94 | ] 95 | 96 | return versions_tags 97 | 98 | 99 | def main(forced_versions: Optional[List[str]] = None) -> None: 100 | # get the latest versions of each repo 101 | wt_version_dicts = get_latest_versions(WEBTREES_REPO, 10, check_assets=True) 102 | my_version_dicts = get_latest_versions(MY_REPO, 10) 103 | 104 | missing_version_dicts = [] 105 | 106 | # go through each version of webtrees 107 | for wt_version_dict in wt_version_dicts: 108 | wt_version = wt_version_dict[VERSION_KEY] 109 | 110 | # dropped support for legacy images 111 | if wt_version.startswith("1."): 112 | continue 113 | 114 | # check if version is a forced one 115 | if wt_version in forced_versions: 116 | # if so, add to list of missing versions 117 | print(f"Version {wt_version} forcefully added.", file=sys.stderr) 118 | missing_version_dicts.append(wt_version_dict) 119 | 120 | # check if version is not in my repo 121 | elif all(v[VERSION_KEY] != wt_version for v in my_version_dicts): 122 | # if not, add to list of missing versions 123 | print(f"Version {wt_version} missing.", file=sys.stderr) 124 | missing_version_dicts.append(wt_version_dict) 125 | 126 | # build authoritative list of all tags we're going to produce 127 | all_tags = get_tags([v[VERSION_KEY] for v in missing_version_dicts]) 128 | 129 | # build output json 130 | builder_list = [] 131 | releaser_list = [] 132 | attester_list = [] 133 | 134 | for missing_version_dict in missing_version_dicts: 135 | ver = missing_version_dict[VERSION_KEY] 136 | 137 | for image in BASE_IMAGES: 138 | attester_list.append({"name": image, "attest_id": ver}) 139 | 140 | builder_list.append( 141 | { 142 | "attest_id": ver, 143 | "platform": ",".join(ARCHITECTURES), 144 | "tags": ",".join(all_tags[ver]), 145 | "webtrees_version": ver, 146 | "php_version": next( 147 | value for key, value in WEBTREES_PHP.items() if ver.startswith(key) 148 | ), 149 | "patch_version": WEBTREES_PATCH.get(ver, WEBTREES_PATCH["default"]), 150 | } 151 | ) 152 | 153 | tag_pretty_list = "\n".join(f"- {tag}" for tag in all_tags[ver]) 154 | releaser_list.append( 155 | { 156 | "tag": ver, 157 | "prerelease": missing_version_dict["prerelease"], 158 | "body": f'Automated release for webtrees version {ver}: {missing_version_dict["html_url"]}\nTags pushed:\n{tag_pretty_list}', 159 | } 160 | ) 161 | 162 | # structure for github actions 163 | output_data = { 164 | "builder": {"include": builder_list}, 165 | "releaser": {"include": releaser_list}, 166 | "attester": {"include": attester_list}, 167 | } 168 | 169 | # save output 170 | print(json.dumps(output_data, indent=4)) 171 | 172 | if github_output := os.getenv("GITHUB_OUTPUT"): 173 | with open(github_output, "w") as fp: 174 | fp.write(f"matrixes={json.dumps(output_data)}") 175 | 176 | 177 | if __name__ == "__main__": 178 | parser = argparse.ArgumentParser() 179 | parser.add_argument( 180 | "--forced", type=str, nargs="*", default=[] 181 | ) # forcefully add specific versions 182 | args = parser.parse_args() 183 | 184 | main(args.forced) 185 | -------------------------------------------------------------------------------- /dev/build_test_version.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | from baker import WEBTREES_PATCH, WEBTREES_PHP 7 | 8 | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 9 | CACHE_FILE = os.path.join(ROOT_DIR, "dev", ".last_built_version") 10 | 11 | 12 | def main(webtrees_version: str) -> None: 13 | with open(CACHE_FILE, "w") as fp: 14 | fp.write(f"{webtrees_version}\n") 15 | 16 | # uncomment the next line to build for arm64 17 | subprocess.run( 18 | [ 19 | "docker", 20 | # "buildx", 21 | "build", 22 | # "--platform", 23 | # "linux/arm64", 24 | "--build-arg", 25 | f"WEBTREES_VERSION={webtrees_version}", 26 | "--build-arg", 27 | f"PHP_VERSION={next(v for k, v in WEBTREES_PHP.items() if webtrees_version.startswith(k))}", 28 | "--build-arg", 29 | f'PATCH_VERSION={WEBTREES_PATCH.get(webtrees_version, WEBTREES_PATCH["default"])}', 30 | "-t", 31 | "webtrees:test", 32 | ".", 33 | ], 34 | cwd=os.path.join(ROOT_DIR, "docker"), 35 | ) 36 | 37 | 38 | if __name__ == "__main__": 39 | parser = argparse.ArgumentParser() 40 | parser.add_argument("--webtrees_version") 41 | args = parser.parse_args() 42 | 43 | if not args.webtrees_version: 44 | # try to load last version 45 | last_version = "" 46 | if os.path.isfile(CACHE_FILE): 47 | with open(CACHE_FILE, "r") as fp: 48 | last_version = fp.read().strip() 49 | 50 | # show last version as default 51 | prompt = "enter the desired webtrees version" 52 | if last_version: 53 | prompt = f"{prompt} ({last_version})" 54 | 55 | # see if user entered anything 56 | args.webtrees_version = input(f"{prompt}: ") or last_version 57 | 58 | if not args.webtrees_version: 59 | sys.exit() 60 | 61 | main(args.webtrees_version) 62 | -------------------------------------------------------------------------------- /dev/requirements.txt: -------------------------------------------------------------------------------- 1 | retry -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | depends_on: 4 | - db 5 | environment: 6 | PRETTY_URLS: "1" 7 | HTTPS: "0" 8 | HTTPS_REDIRECT: "0" 9 | LANG: "en-US" 10 | BASE_URL: "http://localhost" 11 | DB_TYPE: "mysql" 12 | DB_HOST: "db" 13 | DB_PORT: "3306" 14 | DB_USER: "webtrees" 15 | DB_PASS: "baddbpassword" 16 | DB_NAME: "webtrees" 17 | DB_PREFIX: "wt_" 18 | WT_USER: "username" 19 | WT_NAME: "Full Name" 20 | WT_PASS: "badwtpassword" 21 | WT_EMAIL: "me@example.com" 22 | PUID: "1000" 23 | PGID: "1000" 24 | image: ghcr.io/nathanvaughn/webtrees:latest 25 | ports: 26 | - 80:80 27 | # - 443:443 28 | restart: unless-stopped 29 | volumes: 30 | # - ~/certs:/certs/ 31 | - app_data:/var/www/webtrees/data/ 32 | 33 | db: 34 | environment: 35 | MARIADB_DATABASE: "webtrees" 36 | MARIADB_USER: "webtrees" 37 | MARIADB_ROOT_PASSWORD: "badrootpassword" 38 | MARIADB_PASSWORD: "baddbpassword" 39 | # See: https://github.com/NathanVaughn/webtrees-docker/issues/145 40 | image: docker.io/library/mariadb:11 41 | restart: unless-stopped 42 | volumes: 43 | - db_data:/var/lib/mysql 44 | 45 | # db: 46 | # environment: 47 | # POSTGRES_DB: "webtrees" 48 | # POSTGRES_USER: "webtrees" 49 | # POSTGRES_PASSWORD: "badpassword" 50 | # image: docker.io/library/postgres:latest 51 | # restart: unless-stopped 52 | # volumes: 53 | # - db_data:/var/lib/postgresql/data 54 | 55 | volumes: 56 | db_data: 57 | driver: local 58 | app_data: 59 | driver: local 60 | -------------------------------------------------------------------------------- /docker/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-d 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule ^ index.php [L] -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.1 2 | 3 | FROM docker.io/library/php:$PHP_VERSION-apache 4 | 5 | # https://hub.docker.com/_/php 6 | # https://github.com/NathanVaughn/webtrees-docker/issues/160 7 | RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" 8 | 9 | ENV WEBTREES_HOME="/var/www/webtrees" 10 | WORKDIR $WEBTREES_HOME 11 | 12 | # install pre-reqs 13 | # postgresql-client provides pg_isready 14 | # mariadb-client provides mysqladmin 15 | RUN apt-get update \ 16 | && apt-get install -y \ 17 | curl \ 18 | libmagickwand-dev \ 19 | libpq-dev \ 20 | libzip-dev \ 21 | postgresql-client \ 22 | mariadb-client \ 23 | patch \ 24 | python3 \ 25 | unzip \ 26 | --no-install-recommends \ 27 | && rm -rf /var/lib/apt/lists/* 28 | 29 | # install php extensions 30 | # https://github.com/Imagick/imagick/issues/640#issuecomment-2470204174 31 | # This is only an issue on ARM64 32 | ADD --chmod=0755 \ 33 | https://github.com/mlocati/docker-php-extension-installer/releases/download/2.6.3/install-php-extensions \ 34 | /usr/local/bin/ 35 | RUN install-php-extensions imagick/imagick@28f27044e435a2b203e32675e942eb8de620ee58 \ 36 | && docker-php-ext-enable imagick \ 37 | && docker-php-ext-configure gd --with-freetype --with-jpeg \ 38 | && docker-php-ext-install -j"$(nproc)" pdo pdo_mysql pdo_pgsql zip intl gd exif 39 | 40 | # remove old apt stuff 41 | RUN apt-get purge gcc g++ make -y \ 42 | && apt-get autoremove -y \ 43 | && apt-get clean \ 44 | && rm -rf /var/tmp/* /etc/apache2/sites-enabled/000-*.conf 45 | 46 | ARG WEBTREES_VERSION 47 | RUN curl -s -L https://github.com/fisharebest/webtrees/releases/download/${WEBTREES_VERSION}/webtrees-${WEBTREES_VERSION}.zip -o webtrees.zip \ 48 | && unzip -q webtrees.zip -d /var/www/ && rm webtrees.zip \ 49 | && rm $WEBTREES_HOME/*.md 50 | 51 | # Disable version update prompt. Webtrees should not be upgrading itself, 52 | # users should be using tagged container versions 53 | ARG PATCH_VERSION 54 | COPY patches/UpgradeService${PATCH_VERSION}.patch /UpgradeService.patch 55 | RUN patch app/Services/UpgradeService.php /UpgradeService.patch \ 56 | && rm /UpgradeService.patch \ 57 | # Delete file that caused email issues 58 | # https://www.webtrees.net/index.php/fr/forum/help-for-2-0/36616-email-error-after-update-to-2-0-21#89985 59 | # https://github.com/NathanVaughn/webtrees-docker/issues/88 60 | && rm vendor/egulias/email-validator/src/Validation/MessageIDValidation.php 61 | 62 | # enable apache modules 63 | RUN a2enmod rewrite && a2enmod ssl && rm -rf /var/www/html 64 | 65 | # copy apache/php configs 66 | COPY .htaccess ./ 67 | COPY apache/ /etc/apache2/sites-available/ 68 | 69 | # entrypoint 70 | COPY docker-entrypoint.py / 71 | 72 | # healthcheck 73 | COPY docker-healthcheck.sh / 74 | RUN chmod +x /docker-healthcheck.sh 75 | 76 | # final Docker config 77 | EXPOSE 80 443 78 | VOLUME ["$WEBTREES_HOME/data"] 79 | 80 | HEALTHCHECK CMD /docker-healthcheck.sh 81 | ENTRYPOINT ["python3", "/docker-entrypoint.py"] 82 | -------------------------------------------------------------------------------- /docker/apache/webtrees-redir.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName webtrees 3 | DocumentRoot "/var/www/webtrees/" 4 | 5 | RewriteEngine On 6 | RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] 7 | 8 | 9 | Options FollowSymLinks MultiViews 10 | AllowOverride All 11 | Require all granted 12 | 13 | -------------------------------------------------------------------------------- /docker/apache/webtrees-ssl.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName webtrees 3 | DocumentRoot "/var/www/webtrees/" 4 | 5 | SSLEngine on 6 | SSLCertificateFile /certs/webtrees.crt 7 | SSLCertificateKeyFile /certs/webtrees.key 8 | 9 | 10 | Options FollowSymLinks MultiViews 11 | AllowOverride All 12 | Require all granted 13 | 14 | -------------------------------------------------------------------------------- /docker/apache/webtrees.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName webtrees 3 | DocumentRoot "/var/www/webtrees/" 4 | 5 | 6 | Options FollowSymLinks MultiViews 7 | AllowOverride All 8 | Require all granted 9 | 10 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import subprocess 4 | import sys 5 | import time 6 | import urllib.error 7 | from dataclasses import dataclass 8 | from enum import Enum 9 | from typing import Any, List, Literal, Optional, TypeVar, Union, overload 10 | from urllib import request 11 | from urllib.parse import urlencode 12 | 13 | 14 | class NoRedirect(request.HTTPRedirectHandler): 15 | def redirect_request(self, req, fp, code, msg, headers, newurl): 16 | return None 17 | 18 | 19 | class DBType(Enum): 20 | mysql = "mysql" 21 | pgsql = "pgsql" 22 | sqlite = "sqlite" 23 | 24 | 25 | @dataclass 26 | class EnvVars: 27 | prettyurls: bool 28 | https: bool 29 | httpsredirect: bool 30 | sslcertfile: str 31 | sslcertkeyfile: str 32 | lang: str 33 | baseurl: Optional[str] 34 | dbtype: DBType 35 | dbhost: Optional[str] 36 | dbport: str 37 | dbuser: str 38 | dbpass: Optional[str] 39 | dbname: str 40 | tblpfx: str 41 | wtuser: Optional[str] 42 | wtname: Optional[str] 43 | wtpass: Optional[str] 44 | wtemail: Optional[str] 45 | # https://github.com/fisharebest/webtrees/blob/f9a3af650116d75f1a87f454cabff5e9047e43f3/app/Http/Middleware/UseDatabase.php#L71-L82 46 | dbkey: Optional[str] 47 | dbcert: Optional[str] 48 | dbca: Optional[str] 49 | dbverify: bool 50 | # php settings 51 | phpmemorylimit: str 52 | phpmaxexecutiontime: str 53 | phppostmaxsize: str 54 | phpuploadmaxfilesize: str 55 | # user/group ID 56 | puid: str 57 | pgid: str 58 | 59 | 60 | def truish(value: Optional[str]) -> bool: 61 | """ 62 | Check if a value is close enough to true 63 | """ 64 | if value is None: 65 | return False 66 | 67 | return value.lower().strip() in ["true", "yes", "1"] 68 | 69 | 70 | def print2(msg: Any) -> None: 71 | """ 72 | Print a message to stderr. 73 | """ 74 | print(f"[NV_INIT] {msg}", file=sys.stderr) 75 | 76 | 77 | T = TypeVar("T") 78 | 79 | 80 | @overload 81 | def get_environment_variable( 82 | key: str, default: None = None, alternates: Optional[List[str]] = None 83 | ) -> Optional[str]: ... 84 | 85 | 86 | @overload 87 | def get_environment_variable( 88 | key: str, default: T = None, alternates: Optional[List[str]] = None 89 | ) -> T: ... 90 | 91 | 92 | def get_environment_variable( 93 | key: str, default: Optional[T] = None, alternates: Optional[List[str]] = None 94 | ) -> Union[Optional[str], T]: 95 | """ 96 | Try to find the value of an environment variable. 97 | """ 98 | key = key.upper() 99 | 100 | # try to find variable in env 101 | if key in os.environ: 102 | value = os.environ[key] 103 | 104 | print2(f"{key} found in environment variables") 105 | return value 106 | 107 | # try to find file version of variable 108 | file_key = f"{key}_FILE" 109 | 110 | if file_key in os.environ: 111 | # file name does not exist 112 | if not os.path.isfile(os.environ[file_key]): 113 | print(f"WARNING: {file_key} is not a file: {os.environ[file_key]}") 114 | return None 115 | 116 | # read data from file 117 | with open(os.environ[file_key], "r") as f: 118 | value = f.read().strip() 119 | 120 | print2(f"{file_key} found in environment variables") 121 | return value 122 | 123 | # try to find alternate variable 124 | if alternates is not None: 125 | for a in alternates: 126 | a_value = get_environment_variable(a) 127 | if a_value is not None: 128 | return a_value 129 | 130 | # return default value 131 | print2(f"{key} NOT found in environment variables, using default: {default}") 132 | return default 133 | 134 | 135 | ENV = EnvVars( 136 | prettyurls=truish(get_environment_variable("PRETTY_URLS")), 137 | https=truish(get_environment_variable("HTTPS", alternates=["SSL"])), 138 | httpsredirect=truish( 139 | get_environment_variable("HTTPS_REDIRECT", alternates=["SSL_REDIRECT"]) 140 | ), 141 | sslcertfile=get_environment_variable("SSL_CERT_FILE", "/certs/webtrees.crt"), 142 | sslcertkeyfile=get_environment_variable("SSL_CERT_KEY_FILE", "/certs/webtrees.key"), 143 | baseurl=get_environment_variable("BASE_URL"), 144 | lang=get_environment_variable("LANG", "en-US"), 145 | dbtype=DBType[get_environment_variable("DB_TYPE", "mysql")], 146 | dbhost=get_environment_variable("DB_HOST"), 147 | dbport=get_environment_variable("DB_PORT", "3306"), 148 | dbuser=get_environment_variable( 149 | "DB_USER", 150 | "webtrees", 151 | alternates=["MYSQL_USER", "MARIADB_USER", "POSTGRES_USER"], 152 | ), 153 | dbpass=get_environment_variable( 154 | "DB_PASS", 155 | alternates=["MYSQL_PASSWORD", "MARIADB_PASSWORD", "POSTGRES_PASSWORD"], 156 | ), 157 | dbname=get_environment_variable( 158 | "DB_NAME", 159 | default="webtrees", 160 | alternates=["MYSQL_DATABASE", "MARIADB_DATABASE", "POSTGRES_DB"], 161 | ), 162 | tblpfx=get_environment_variable("DB_PREFIX", "wt_"), 163 | wtuser=get_environment_variable("WT_USER"), 164 | wtname=get_environment_variable("WT_NAME"), 165 | wtpass=get_environment_variable("WT_PASS"), 166 | wtemail=get_environment_variable("WT_EMAIL"), 167 | dbkey=get_environment_variable("DB_KEY"), 168 | dbcert=get_environment_variable("DB_CERT"), 169 | dbca=get_environment_variable("DB_CA"), 170 | dbverify=truish(get_environment_variable("DB_VERIFY")), 171 | phpmemorylimit=get_environment_variable("PHP_MEMORY_LIMIT", "1024M"), 172 | phpmaxexecutiontime=get_environment_variable("PHP_MAX_EXECUTION_TIME", "90"), 173 | phppostmaxsize=get_environment_variable("PHP_POST_MAX_SIZE", "50M"), 174 | phpuploadmaxfilesize=get_environment_variable("PHP_UPLOAD_MAX_FILE_SIZE", "50M"), 175 | puid=get_environment_variable("PUID", "33"), # www-data user 176 | pgid=get_environment_variable("PGID", "33"), 177 | ) 178 | 179 | 180 | ROOT = "/var/www/webtrees" 181 | DATA_DIR = os.path.join(ROOT, "data") 182 | CONFIG_FILE = os.path.join(DATA_DIR, "config.ini.php") 183 | PHP_INI_FILE = "/usr/local/etc/php/php.ini" 184 | 185 | os.chdir(ROOT) 186 | 187 | 188 | def retry_urlopen(url: str, data: bytes) -> None: 189 | """ 190 | Retry a request until a postiive repsonse code is reached. Raises error if it fails. 191 | """ 192 | opener = request.build_opener(NoRedirect) 193 | request.install_opener(opener) 194 | 195 | for try_ in range(10): 196 | try: 197 | # make request 198 | print2(f"Attempt {try_} for {url}") 199 | resp = request.urlopen(url, data) 200 | except urllib.error.HTTPError as e: 201 | # capture error as well 202 | resp = e 203 | print2(f"Recieved HTTP {resp.status} response") 204 | 205 | # check status code 206 | # 302 is also accpetable in case the user selected something other than port 80 207 | if resp.status in (200, 302): 208 | return 209 | 210 | # backoff 211 | time.sleep(try_) 212 | 213 | raise RuntimeError(f"Could not send a request to {url}") 214 | 215 | 216 | def add_line_to_file(filename: str, newline: str) -> None: 217 | """ 218 | Add a new line to a file. If an existing line is found with the same 219 | starting string, it will be replaced. 220 | """ 221 | newline += "\n" 222 | 223 | # read file 224 | with open(filename, "r") as fp: 225 | lines = fp.readlines() 226 | 227 | key = newline.split("=")[0] 228 | 229 | # replace matching line 230 | found = False 231 | 232 | for i, line in enumerate(lines): 233 | if line.startswith(key): 234 | if line == newline: 235 | return 236 | 237 | lines[i] = newline 238 | found = True 239 | break 240 | 241 | if not found: 242 | lines.append(newline) 243 | 244 | # write new contents 245 | with open(filename, "w") as fp: 246 | fp.writelines(lines) 247 | 248 | 249 | def set_config_value(key: str, value: Optional[str]) -> None: 250 | """ 251 | In the config file, make sure the given key is set to the given value. 252 | """ 253 | if value is None: 254 | return 255 | 256 | print2(f"Setting value for {key} in config") 257 | 258 | if not os.path.isfile(CONFIG_FILE): 259 | print2(f"WARNING: {CONFIG_FILE} does not exist") 260 | return 261 | 262 | add_line_to_file(CONFIG_FILE, f'{key}="{value}"') 263 | 264 | 265 | def set_php_ini_value(key: str, value: str) -> None: 266 | """ 267 | In the php.ini file, make sure the given key is set to the given value. 268 | """ 269 | print2(f"Setting value for {key} in php.ini") 270 | add_line_to_file(PHP_INI_FILE, f"{key} = {value}") 271 | 272 | 273 | def enable_apache_site( 274 | enable_sites: List[Literal["webtrees", "webtrees-redir", "webtrees-ssl"]], 275 | ) -> None: 276 | """ 277 | Enable an Apache site. 278 | """ 279 | 280 | # update ssl apache config with cert path from env 281 | ssl_site_file = "/etc/apache2/sites-available/webtrees-ssl.conf" 282 | 283 | # make paths absolute 284 | if not os.path.isabs(ENV.sslcertfile): 285 | ENV.sslcertfile = os.path.join(DATA_DIR, ENV.sslcertfile) 286 | 287 | if not os.path.isabs(ENV.sslcertkeyfile): 288 | ENV.sslcertkeyfile = os.path.join(DATA_DIR, ENV.sslcertkeyfile) 289 | 290 | # update file 291 | with open(ssl_site_file, "r") as fp: 292 | ssl_site_file_lines = fp.readlines() 293 | 294 | new_ssl_site_file_lines = [] 295 | for line in ssl_site_file_lines: 296 | if line.strip().startswith("SSLCertificateFile"): 297 | line = line.replace(line.split()[1], ENV.sslcertfile) 298 | elif line.strip().startswith("SSLCertificateKeyFile"): 299 | line = line.replace(line.split()[1], ENV.sslcertkeyfile) 300 | 301 | new_ssl_site_file_lines.append(line) 302 | 303 | with open(ssl_site_file, "w") as fp: 304 | fp.writelines(new_ssl_site_file_lines) 305 | 306 | all_sites = ["webtrees", "webtrees-redir", "webtrees-ssl"] 307 | 308 | # perl complains about locale to stderr, so disable that 309 | 310 | # disable the other sites 311 | for s in all_sites: 312 | if s not in enable_sites: 313 | print2(f"Disabling site {s}") 314 | subprocess.check_call( 315 | ["a2dissite", s], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL 316 | ) 317 | 318 | # enable the desired sites 319 | for s in enable_sites: 320 | print2(f"Enabling site {s}") 321 | subprocess.check_call( 322 | ["a2ensite", s], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL 323 | ) 324 | 325 | 326 | def perms() -> None: 327 | """ 328 | Set up folder permissions 329 | """ 330 | 331 | print2("Setting up folder permissions for uploads") 332 | # https://github.com/linuxserver/docker-baseimage-alpine/blob/bef0f4cee208396c92c0fdd1426613de02698301/root/etc/s6-overlay/s6-rc.d/init-adduser/run#L4-L9 333 | subprocess.check_call(["groupmod", "-o", "-g", ENV.pgid, "www-data"]) 334 | subprocess.check_call(["usermod", "-o", "-u", ENV.puid, "www-data"]) 335 | subprocess.check_call(["chown", "-R", "www-data:www-data", DATA_DIR]) 336 | 337 | if os.path.isfile(CONFIG_FILE): 338 | subprocess.check_call(["chmod", "700", CONFIG_FILE]) 339 | 340 | 341 | def php_ini() -> None: 342 | """ 343 | Update PHP .ini file 344 | """ 345 | print2("Updating php.ini") 346 | 347 | if not os.path.isfile(PHP_INI_FILE): 348 | print2("Creating php.ini") 349 | 350 | os.makedirs(os.path.dirname(PHP_INI_FILE), exist_ok=True) 351 | with open(PHP_INI_FILE, "w") as fp: 352 | fp.writelines(["[PHP]\n", "\n"]) 353 | 354 | set_php_ini_value("memory_limit", ENV.phpmemorylimit) 355 | set_php_ini_value("max_execution_time", ENV.phpmaxexecutiontime) 356 | set_php_ini_value("post_max_size", ENV.phppostmaxsize) 357 | set_php_ini_value("upload_max_filesize", ENV.phpuploadmaxfilesize) 358 | 359 | 360 | def check_db_variables() -> bool: 361 | """ 362 | Check if all required database variables are present 363 | """ 364 | try: 365 | assert ENV.dbtype is not None 366 | assert ENV.dbname is not None 367 | assert ENV.tblpfx is not None 368 | 369 | if ENV.dbtype == DBType.sqlite: 370 | ENV.dbhost = "" 371 | ENV.dbport = "" 372 | ENV.dbuser = "" 373 | ENV.dbpass = "" 374 | 375 | assert ENV.dbhost is not None 376 | assert ENV.dbport is not None 377 | assert ENV.dbuser is not None 378 | assert ENV.dbpass is not None 379 | 380 | except AssertionError: 381 | print2("WARNING: Not all database variables are set") 382 | return False 383 | 384 | return True 385 | 386 | 387 | def setup_wizard() -> None: 388 | """ 389 | Run the setup wizard 390 | """ 391 | 392 | if os.path.isfile(CONFIG_FILE): 393 | return 394 | 395 | print2("Attempting to automate setup wizard") 396 | 397 | # make sure all the variables we need are not set to None 398 | if not check_db_variables(): 399 | return 400 | 401 | if any( 402 | v is None 403 | for v in [ENV.baseurl, ENV.wtname, ENV.wtuser, ENV.wtpass, ENV.wtemail] 404 | ): 405 | print2("WARNING: Not all required variables were found for setup wizard") 406 | return 407 | 408 | assert ENV.baseurl is not None 409 | if not ENV.baseurl.startswith("http"): 410 | print2( 411 | "WARNING: BASE_URL does not start with 'http'. This is likely not what you want." 412 | ) 413 | 414 | print2("Automating setup wizard") 415 | print2("Starting Apache in background") 416 | # set us up to a known HTTP state 417 | enable_apache_site(["webtrees"]) 418 | # run apache in the background 419 | apache_proc = subprocess.Popen(["apache2-foreground"], stderr=subprocess.DEVNULL) 420 | 421 | if ENV.dbtype in [DBType.mysql, DBType.pgsql]: 422 | # for typing, check_db_variables already does this 423 | assert ENV.dbhost is not None 424 | 425 | # try to resolve the host 426 | # most common error is wrong hostname 427 | try: 428 | socket.gethostbyname(ENV.dbhost) 429 | except socket.gaierror: 430 | print2(f"ERROR: Could not resolve database host '{ENV.dbhost}'") 431 | print2( 432 | "ERROR: You likely have the DBHOST environment variable set incorrectly." 433 | ) 434 | print2("ERROR: Exiting.") 435 | 436 | # stop apache 437 | apache_proc.terminate() 438 | # die 439 | sys.exit(1) 440 | 441 | # wait until database is ready 442 | if ENV.dbtype == DBType.mysql: 443 | # https://dev.mysql.com/doc/refman/8.0/en/mysqladmin.html#option_mysqladmin_user 444 | # don't miss the capital P 445 | cmd = ["mysqladmin", "ping", "-h", ENV.dbhost, "-P", ENV.dbport, "--silent"] 446 | name = "MySQL" 447 | elif ENV.dbtype == DBType.pgsql: 448 | # https://www.postgresql.org/docs/current/app-pg-isready.html 449 | cmd = ["pg_isready", "-h", ENV.dbhost, "-p", ENV.dbport, "--quiet"] 450 | name = "PostgreSQL" 451 | 452 | while subprocess.run(cmd).returncode != 0: 453 | print2(f"Waiting for {name} server {ENV.dbhost}:{ENV.dbport} to be ready") 454 | time.sleep(1) 455 | 456 | else: 457 | # sqlite 458 | # let Apache start up 459 | time.sleep(2) 460 | 461 | # send it 462 | url = "http://127.0.0.1:80/" 463 | print2(f"Sending setup wizard request to {url}") 464 | 465 | retry_urlopen( 466 | url, 467 | urlencode( 468 | { 469 | "lang": ENV.lang, 470 | "tblpfx": ENV.tblpfx, 471 | "baseurl": ENV.baseurl, 472 | "dbtype": ENV.dbtype.value, 473 | "dbhost": ENV.dbhost, 474 | "dbport": ENV.dbport, 475 | "dbuser": ENV.dbuser, 476 | "dbpass": ENV.dbpass, 477 | "dbname": ENV.dbname, 478 | "wtname": ENV.wtname, 479 | "wtuser": ENV.wtuser, 480 | "wtpass": ENV.wtpass, 481 | "wtemail": ENV.wtemail, 482 | "step": "6", 483 | } 484 | ).encode("ascii"), 485 | ) 486 | 487 | print2("Stopping Apache") 488 | apache_proc.terminate() 489 | 490 | 491 | def update_config_file() -> None: 492 | """ 493 | Update the config file with items set via environment variables 494 | """ 495 | print2("Updating config file") 496 | 497 | if not os.path.isfile(CONFIG_FILE): 498 | print2(f"Config file not found at {CONFIG_FILE}. Nothing to update.") 499 | return 500 | 501 | # update independent values 502 | set_config_value("rewrite_urls", str(int(ENV.prettyurls))) 503 | set_config_value("base_url", ENV.baseurl) 504 | 505 | # update database values as a group 506 | if check_db_variables(): 507 | set_config_value("dbtype", ENV.dbtype.value) 508 | set_config_value("dbhost", ENV.dbhost) 509 | set_config_value("dbport", ENV.dbport) 510 | set_config_value("dbuser", ENV.dbuser) 511 | set_config_value("dbpass", ENV.dbpass) 512 | set_config_value("dbname", ENV.dbname) 513 | set_config_value("tblpfx", ENV.tblpfx) 514 | 515 | # update databases verification values 516 | if ENV.dbtype == DBType.mysql and all( 517 | v is not None for v in [ENV.dbkey, ENV.dbcert, ENV.dbca] 518 | ): 519 | set_config_value("dbkey", ENV.dbkey) 520 | set_config_value("dbcert", ENV.dbcert) 521 | set_config_value("dbca", ENV.dbca) 522 | set_config_value("dbverify", str(int(ENV.dbverify))) 523 | 524 | 525 | def https() -> None: 526 | """ 527 | Configure enabled Apache sites 528 | """ 529 | print2("Configuring HTTPS") 530 | 531 | # no https 532 | if not ENV.https: 533 | print2("Removing HTTPS") 534 | enable_apache_site(["webtrees"]) 535 | # https with redirect 536 | elif ENV.httpsredirect: 537 | print2("Adding HTTPS, with HTTPS redirect") 538 | enable_apache_site(["webtrees-ssl", "webtrees-redir"]) 539 | # https no redirect 540 | else: 541 | print2("Adding HTTPS, removing HTTPS redirect") 542 | enable_apache_site(["webtrees", "webtrees-ssl"]) 543 | 544 | 545 | def htaccess() -> None: 546 | """ 547 | Recreate .htaccess file if it ever deletes itself in the /data/ directory 548 | """ 549 | htaccess_file = os.path.join(DATA_DIR, ".htaccess") 550 | 551 | if os.path.isfile(htaccess_file): 552 | return 553 | 554 | print2(f"WARNING: {htaccess_file} does not exist") 555 | 556 | with open(htaccess_file, "w") as fp: 557 | fp.writelines(["order allow,deny", "deny from all"]) 558 | 559 | print2(f"Created {htaccess_file}") 560 | 561 | 562 | def main() -> None: 563 | # first, set up permissions 564 | perms() 565 | # create php config 566 | php_ini() 567 | # run the setup wizard if the config file doesn't exist 568 | setup_wizard() 569 | # update the config file 570 | update_config_file() 571 | # configure https 572 | https() 573 | # make sure .htaccess exists 574 | htaccess() 575 | # set up permissions again 576 | perms() 577 | 578 | print2("Starting Apache") 579 | subprocess.run(["apache2-foreground"], stderr=subprocess.DEVNULL) 580 | 581 | 582 | if __name__ == "__main__": 583 | main() 584 | -------------------------------------------------------------------------------- /docker/docker-healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ls /etc/apache2/sites-enabled | grep -q 'ssl'; 4 | then 5 | curl -vs -k --fail https://127.0.0.1:443/ || exit 1 6 | else 7 | curl -vs --fail http://127.0.0.1:80/ || exit 1 8 | fi 9 | -------------------------------------------------------------------------------- /docker/patches/UpgradeService1.patch: -------------------------------------------------------------------------------- 1 | diff --git "a/.\\UpgradeService.php" "b/.\\UpgradeServicePatched.php" 2 | index b69bea0..c3e682c 100644 3 | --- "a/.\\UpgradeService.php" 4 | +++ "b/.\\UpgradeServicePatched.php" 5 | @@ -339,34 +339,6 @@ class UpgradeService 6 | */ 7 | private function fetchLatestVersion(bool $force): string 8 | { 9 | - $last_update_timestamp = (int) Site::getPreference('LATEST_WT_VERSION_TIMESTAMP'); 10 | - 11 | - $current_timestamp = time(); 12 | - 13 | - if ($force || $last_update_timestamp < $current_timestamp - self::CHECK_FOR_UPDATE_INTERVAL) { 14 | - try { 15 | - $client = new Client([ 16 | - 'timeout' => self::HTTP_TIMEOUT, 17 | - ]); 18 | - 19 | - $response = $client->get(self::UPDATE_URL, [ 20 | - 'query' => $this->serverParameters(), 21 | - ]); 22 | - 23 | - if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) { 24 | - Site::setPreference('LATEST_WT_VERSION', $response->getBody()->getContents()); 25 | - Site::setPreference('LATEST_WT_VERSION_TIMESTAMP', (string) $current_timestamp); 26 | - Site::setPreference('LATEST_WT_VERSION_ERROR', ''); 27 | - } else { 28 | - Site::setPreference('LATEST_WT_VERSION_ERROR', 'HTTP' . $response->getStatusCode()); 29 | - } 30 | - } catch (GuzzleException $ex) { 31 | - // Can't connect to the server? 32 | - // Use the existing information about latest versions. 33 | - Site::setPreference('LATEST_WT_VERSION_ERROR', $ex->getMessage()); 34 | - } 35 | - } 36 | - 37 | return Site::getPreference('LATEST_WT_VERSION'); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /docker/patches/UpgradeService2.patch: -------------------------------------------------------------------------------- 1 | diff --git "a/.\\UpgradeService.php" "b/.\\UpgradeServicePatched.php" 2 | index 6bb3504..a630dfb 100644 3 | --- "a/.\\UpgradeService.php" 4 | +++ "b/.\\UpgradeServicePatched.php" 5 | @@ -337,34 +337,6 @@ class UpgradeService 6 | */ 7 | private function fetchLatestVersion(bool $force): string 8 | { 9 | - $last_update_timestamp = (int) Site::getPreference('LATEST_WT_VERSION_TIMESTAMP'); 10 | - 11 | - $current_timestamp = time(); 12 | - 13 | - if ($force || $last_update_timestamp < $current_timestamp - self::CHECK_FOR_UPDATE_INTERVAL) { 14 | - Site::setPreference('LATEST_WT_VERSION_TIMESTAMP', (string) $current_timestamp); 15 | - 16 | - try { 17 | - $client = new Client([ 18 | - 'timeout' => self::HTTP_TIMEOUT, 19 | - ]); 20 | - 21 | - $response = $client->get(self::UPDATE_URL, [ 22 | - 'query' => $this->serverParameters(), 23 | - ]); 24 | - 25 | - if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) { 26 | - Site::setPreference('LATEST_WT_VERSION', $response->getBody()->getContents()); 27 | - Site::setPreference('LATEST_WT_VERSION_ERROR', ''); 28 | - } else { 29 | - Site::setPreference('LATEST_WT_VERSION_ERROR', 'HTTP' . $response->getStatusCode()); 30 | - } 31 | - } catch (GuzzleException $ex) { 32 | - // Can't connect to the server? 33 | - // Use the existing information about latest versions. 34 | - Site::setPreference('LATEST_WT_VERSION_ERROR', $ex->getMessage()); 35 | - } 36 | - } 37 | 38 | return Site::getPreference('LATEST_WT_VERSION'); 39 | } 40 | --------------------------------------------------------------------------------