├── .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)
4 | [](https://hub.docker.com/r/nathanvaughn/webtrees)
5 | [](https://hub.docker.com/r/nathanvaughn/webtrees)
6 | [](https://hub.docker.com/r/nathanvaughn/webtrees)
7 | [](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 |
--------------------------------------------------------------------------------