build-wagtail 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: build-wagtail
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dockerise a Wagtail project in place with one command.
|
|
5
|
+
Keywords: wagtail,django,docker,deployment
|
|
6
|
+
Author: Kailash Nadh
|
|
7
|
+
Author-email: Kailash Nadh <kailash@nadh.in>
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Framework :: Wagtail
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# build-wagtail
|
|
17
|
+
|
|
18
|
+
Dockerise a Wagtail project in place with one command. It generates a
|
|
19
|
+
Dockerfile, settings overrides and a `compose.yml`, copies the site's database
|
|
20
|
+
and media into `./data`, and builds the image. The app runs on
|
|
21
|
+
[granian](https://github.com/emmett-framework/granian).
|
|
22
|
+
|
|
23
|
+
Needs Docker with the compose plugin. The tool itself has no dependencies.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
uv tool install build-wagtail # or: pipx install build-wagtail
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Run it from the Wagtail project root (where `manage.py` is):
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
build-wagtail example.org,www.example.org
|
|
37
|
+
docker compose up -d
|
|
38
|
+
docker compose exec web python manage.py createsuperuser
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The first hostname is canonical. The site is served on `127.0.0.1:8000`; put a
|
|
42
|
+
reverse proxy in front of it for TLS. By default the host's proxy is expected to
|
|
43
|
+
serve `/static` and `/media` from `./data`, and the tool prints an example Caddy
|
|
44
|
+
config for that. With `--caddy`, a Caddy container serves them instead.
|
|
45
|
+
|
|
46
|
+
| Option | Default | |
|
|
47
|
+
|-------------------|-------------------------------------------------|----------------------------------------------------|
|
|
48
|
+
| `--port` | `8000` | Localhost port the site is served on |
|
|
49
|
+
| `--caddy` | off | Run Caddy in a container for static/media |
|
|
50
|
+
| `--workers` | `3` | granian workers |
|
|
51
|
+
| `--python` | `.python-version`, else `3.13` | Python version of the image |
|
|
52
|
+
| `--settings` | from `manage.py`, or `<project>.settings.production` | Settings module to build on |
|
|
53
|
+
| `--no-build` | off | Write the files but skip `docker compose build` |
|
|
54
|
+
|
|
55
|
+
The project needs a `uv.lock`, `pyproject.toml` or `requirements.txt`.
|
|
56
|
+
|
|
57
|
+
## What it writes
|
|
58
|
+
|
|
59
|
+
- `.env`: secret key, hostnames and the container user. Created once, never overwritten.
|
|
60
|
+
- `data/`: the live database, media and collected static files. Seeded once, never overwritten. Back it up.
|
|
61
|
+
- `.wagtail-docker/` and `compose.yml`: regenerated on every run. Options aren't
|
|
62
|
+
stored, so pass the same ones each time.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# build-wagtail
|
|
2
|
+
|
|
3
|
+
Dockerise a Wagtail project in place with one command. It generates a
|
|
4
|
+
Dockerfile, settings overrides and a `compose.yml`, copies the site's database
|
|
5
|
+
and media into `./data`, and builds the image. The app runs on
|
|
6
|
+
[granian](https://github.com/emmett-framework/granian).
|
|
7
|
+
|
|
8
|
+
Needs Docker with the compose plugin. The tool itself has no dependencies.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```shell
|
|
13
|
+
uv tool install build-wagtail # or: pipx install build-wagtail
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Run it from the Wagtail project root (where `manage.py` is):
|
|
19
|
+
|
|
20
|
+
```shell
|
|
21
|
+
build-wagtail example.org,www.example.org
|
|
22
|
+
docker compose up -d
|
|
23
|
+
docker compose exec web python manage.py createsuperuser
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The first hostname is canonical. The site is served on `127.0.0.1:8000`; put a
|
|
27
|
+
reverse proxy in front of it for TLS. By default the host's proxy is expected to
|
|
28
|
+
serve `/static` and `/media` from `./data`, and the tool prints an example Caddy
|
|
29
|
+
config for that. With `--caddy`, a Caddy container serves them instead.
|
|
30
|
+
|
|
31
|
+
| Option | Default | |
|
|
32
|
+
|-------------------|-------------------------------------------------|----------------------------------------------------|
|
|
33
|
+
| `--port` | `8000` | Localhost port the site is served on |
|
|
34
|
+
| `--caddy` | off | Run Caddy in a container for static/media |
|
|
35
|
+
| `--workers` | `3` | granian workers |
|
|
36
|
+
| `--python` | `.python-version`, else `3.13` | Python version of the image |
|
|
37
|
+
| `--settings` | from `manage.py`, or `<project>.settings.production` | Settings module to build on |
|
|
38
|
+
| `--no-build` | off | Write the files but skip `docker compose build` |
|
|
39
|
+
|
|
40
|
+
The project needs a `uv.lock`, `pyproject.toml` or `requirements.txt`.
|
|
41
|
+
|
|
42
|
+
## What it writes
|
|
43
|
+
|
|
44
|
+
- `.env`: secret key, hostnames and the container user. Created once, never overwritten.
|
|
45
|
+
- `data/`: the live database, media and collected static files. Seeded once, never overwritten. Back it up.
|
|
46
|
+
- `.wagtail-docker/` and `compose.yml`: regenerated on every run. Options aren't
|
|
47
|
+
stored, so pass the same ones each time.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "build-wagtail"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Dockerise a Wagtail project in place with one command."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
dependencies = []
|
|
8
|
+
keywords = [
|
|
9
|
+
"wagtail",
|
|
10
|
+
"django",
|
|
11
|
+
"docker",
|
|
12
|
+
"deployment",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Framework :: Django",
|
|
17
|
+
"Framework :: Wagtail",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: System :: Installation/Setup",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[[project.authors]]
|
|
23
|
+
name = "Kailash Nadh"
|
|
24
|
+
email = "kailash@nadh.in"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
build-wagtail = "build_wagtail:main"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["uv_build>=0.8,<0.14"]
|
|
31
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "build-wagtail"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Dockerise a Wagtail project in place with one command."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Kailash Nadh", email = "kailash@nadh.in" }]
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = []
|
|
9
|
+
keywords = ["wagtail", "django", "docker", "deployment"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Framework :: Django",
|
|
13
|
+
"Framework :: Wagtail",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: System :: Installation/Setup",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
build-wagtail = "build_wagtail:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.8,<0.14"]
|
|
23
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Dockerise a Wagtail project in place.
|
|
2
|
+
|
|
3
|
+
Run it from the project root (where manage.py is). It writes .wagtail-docker/,
|
|
4
|
+
compose.yml and .env, copies the site's data into ./data, and builds the image.
|
|
5
|
+
After that, use `docker compose up -d` and `docker compose down`.
|
|
6
|
+
|
|
7
|
+
build-wagtail example.org,www.example.org
|
|
8
|
+
build-wagtail example.org --port 8001 --workers 4 --caddy
|
|
9
|
+
|
|
10
|
+
By default, only the app server runs in Docker, on 127.0.0.1:PORT, and the
|
|
11
|
+
host's reverse proxy serves /static and /media straight from ./data. With
|
|
12
|
+
--caddy, a Caddy container serves them and proxies the rest to the app.
|
|
13
|
+
|
|
14
|
+
Re-running is safe: .env and data/ are never overwritten, generated files are.
|
|
15
|
+
Hostnames are only needed on the first run, and update .env when given later.
|
|
16
|
+
The other options aren't stored, so pass the same ones on every run.
|
|
17
|
+
Needs only the Python standard library.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import secrets
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
MARKER = "# Generated by build-wagtail"
|
|
30
|
+
|
|
31
|
+
# Files and directories in the project, relative to its root.
|
|
32
|
+
MANAGE_PY = Path("manage.py")
|
|
33
|
+
PYTHON_VERSION_FILE = Path(".python-version")
|
|
34
|
+
DB_FILE = "db.sqlite3"
|
|
35
|
+
MEDIA_DIR = "media"
|
|
36
|
+
STATIC_DIR = "static"
|
|
37
|
+
|
|
38
|
+
# What this script writes into the project.
|
|
39
|
+
GEN = Path(".wagtail-docker")
|
|
40
|
+
ENV_FILE = Path(".env")
|
|
41
|
+
COMPOSE_FILE = Path("compose.yml")
|
|
42
|
+
DATA_DIR = Path("data")
|
|
43
|
+
|
|
44
|
+
# Paths and names inside the containers.
|
|
45
|
+
APP_DIR = "/app"
|
|
46
|
+
CONTAINER_DATA_DIR = "/data"
|
|
47
|
+
CADDY_STATIC_DIR = "/srv/static"
|
|
48
|
+
CADDY_MEDIA_DIR = "/srv/media"
|
|
49
|
+
APP_PORT = 8000
|
|
50
|
+
SETTINGS_MODULE = "wagtail_docker_settings"
|
|
51
|
+
|
|
52
|
+
UV_IMAGE = "ghcr.io/astral-sh/uv:python{python}-bookworm-slim"
|
|
53
|
+
CADDY_IMAGE = "caddy:2.10-alpine"
|
|
54
|
+
|
|
55
|
+
DEFAULT_PORT = 8000
|
|
56
|
+
DEFAULT_WORKERS = 3
|
|
57
|
+
DEFAULT_PYTHON = "3.13"
|
|
58
|
+
|
|
59
|
+
# Containers shouldn't run as root. Used instead when the script is run by root.
|
|
60
|
+
FALLBACK_UID = FALLBACK_GID = 1000
|
|
61
|
+
|
|
62
|
+
# Dependency files, in order of preference, and the Dockerfile lines to install from each.
|
|
63
|
+
DEP_INSTALLERS = [
|
|
64
|
+
("uv.lock", "COPY pyproject.toml uv.lock ./\nRUN uv sync --frozen --no-dev --no-install-project"),
|
|
65
|
+
("pyproject.toml", "COPY pyproject.toml ./\nRUN uv sync --no-dev --no-install-project"),
|
|
66
|
+
("requirements.txt", "COPY requirements.txt ./\nRUN uv venv && uv pip install -r requirements.txt"),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def die(msg):
|
|
71
|
+
sys.exit(f"error: {msg}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_args():
|
|
75
|
+
p = argparse.ArgumentParser(description="Dockerise a Wagtail project in place. Run from the project root.")
|
|
76
|
+
p.add_argument("hosts", nargs="?",
|
|
77
|
+
help="comma-separated hostnames, the first one canonical. Required on the first run.")
|
|
78
|
+
p.add_argument("--port", type=int, default=DEFAULT_PORT, help="localhost port the site is served on (default: %(default)s)")
|
|
79
|
+
p.add_argument("--caddy", action="store_true",
|
|
80
|
+
help="run Caddy in a container to serve /static and /media and proxy the app")
|
|
81
|
+
p.add_argument("--workers", type=int, default=DEFAULT_WORKERS, help="granian workers (default: %(default)s)")
|
|
82
|
+
p.add_argument("--python", help=f"Python version (default: {PYTHON_VERSION_FILE}, else {DEFAULT_PYTHON})")
|
|
83
|
+
p.add_argument("--settings",
|
|
84
|
+
help="settings module to build on (default: from manage.py, or <project>.settings.production)")
|
|
85
|
+
p.add_argument("--no-build", action="store_true", help="write the files but don't run docker compose build")
|
|
86
|
+
args = p.parse_args()
|
|
87
|
+
|
|
88
|
+
if not 0 < args.port < 65536:
|
|
89
|
+
p.error("--port must be between 1 and 65535.")
|
|
90
|
+
if args.workers < 1:
|
|
91
|
+
p.error("--workers must be at least 1.")
|
|
92
|
+
return args
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def find_settings(override):
|
|
96
|
+
"""Return (project package, settings module to build on)."""
|
|
97
|
+
m = re.search(r"""DJANGO_SETTINGS_MODULE["'],\s*["']([^"']+)["']""", MANAGE_PY.read_text())
|
|
98
|
+
if not m:
|
|
99
|
+
die(f"couldn't find DJANGO_SETTINGS_MODULE in {MANAGE_PY}.")
|
|
100
|
+
|
|
101
|
+
settings = m.group(1)
|
|
102
|
+
project = settings.split(".")[0]
|
|
103
|
+
if not Path(project, "wsgi.py").is_file():
|
|
104
|
+
die(f"{project}/wsgi.py not found.")
|
|
105
|
+
|
|
106
|
+
if override:
|
|
107
|
+
return project, override
|
|
108
|
+
if Path(project, "settings", "production.py").is_file():
|
|
109
|
+
settings = f"{project}.settings.production"
|
|
110
|
+
return project, settings
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def find_python(override):
|
|
114
|
+
if override:
|
|
115
|
+
return override
|
|
116
|
+
if PYTHON_VERSION_FILE.is_file():
|
|
117
|
+
return PYTHON_VERSION_FILE.read_text().split()[0]
|
|
118
|
+
return DEFAULT_PYTHON
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def deps_step():
|
|
122
|
+
"""Return the Dockerfile lines that install the project's dependencies."""
|
|
123
|
+
for name, step in DEP_INSTALLERS:
|
|
124
|
+
if Path(name).is_file():
|
|
125
|
+
return step
|
|
126
|
+
die(f"none of {', '.join(n for n, _ in DEP_INSTALLERS)} found.")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_env():
|
|
130
|
+
env = {}
|
|
131
|
+
for line in ENV_FILE.read_text().splitlines():
|
|
132
|
+
if "=" in line and not line.lstrip().startswith("#"):
|
|
133
|
+
k, v = line.split("=", 1)
|
|
134
|
+
env[k.strip()] = v.strip()
|
|
135
|
+
return env
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def create_env(hosts):
|
|
139
|
+
if os.getuid() == 0:
|
|
140
|
+
uid, gid = FALLBACK_UID, FALLBACK_GID
|
|
141
|
+
else:
|
|
142
|
+
uid, gid = os.getuid(), os.getgid()
|
|
143
|
+
|
|
144
|
+
body = (
|
|
145
|
+
f"DJANGO_SECRET_KEY={secrets.token_urlsafe(50)}\n"
|
|
146
|
+
"# Comma-separated. The first one is canonical.\n"
|
|
147
|
+
f"DJANGO_ALLOWED_HOSTS={hosts}\n"
|
|
148
|
+
f"PUID={uid}\n"
|
|
149
|
+
f"PGID={gid}\n"
|
|
150
|
+
)
|
|
151
|
+
fd = os.open(ENV_FILE, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
152
|
+
with os.fdopen(fd, "w") as f:
|
|
153
|
+
f.write(body)
|
|
154
|
+
print(f"Created {ENV_FILE}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def update_env_hosts(hosts):
|
|
158
|
+
text = ENV_FILE.read_text()
|
|
159
|
+
new, n = re.subn(r"(?m)^DJANGO_ALLOWED_HOSTS=.*$", lambda _: f"DJANGO_ALLOWED_HOSTS={hosts}", text)
|
|
160
|
+
if n == 0:
|
|
161
|
+
new = text.rstrip("\n") + f"\nDJANGO_ALLOWED_HOSTS={hosts}\n"
|
|
162
|
+
if new != text:
|
|
163
|
+
ENV_FILE.write_text(new)
|
|
164
|
+
print(f"Updated DJANGO_ALLOWED_HOSTS in {ENV_FILE}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def setup_data(uid, gid):
|
|
168
|
+
"""Seed the data dir from the project's db and media on the first run."""
|
|
169
|
+
(DATA_DIR / STATIC_DIR).mkdir(parents=True, exist_ok=True)
|
|
170
|
+
|
|
171
|
+
if not (DATA_DIR / DB_FILE).exists() and Path(DB_FILE).is_file():
|
|
172
|
+
shutil.copy2(DB_FILE, DATA_DIR / DB_FILE)
|
|
173
|
+
print(f"Copied {DB_FILE} to {DATA_DIR / DB_FILE}")
|
|
174
|
+
|
|
175
|
+
if not (DATA_DIR / MEDIA_DIR).exists():
|
|
176
|
+
if Path(MEDIA_DIR).is_dir():
|
|
177
|
+
shutil.copytree(MEDIA_DIR, DATA_DIR / MEDIA_DIR, symlinks=True)
|
|
178
|
+
print(f"Copied {MEDIA_DIR}/ to {DATA_DIR / MEDIA_DIR}/")
|
|
179
|
+
else:
|
|
180
|
+
(DATA_DIR / MEDIA_DIR).mkdir()
|
|
181
|
+
|
|
182
|
+
if os.getuid() == 0:
|
|
183
|
+
for p in [DATA_DIR, *DATA_DIR.rglob("*")]:
|
|
184
|
+
os.chown(p, uid, gid, follow_symlinks=False)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def settings_py(settings):
|
|
188
|
+
return f'''{MARKER}. Docker overrides on top of the project's own settings.
|
|
189
|
+
import os
|
|
190
|
+
|
|
191
|
+
from {settings} import * # noqa: F401,F403
|
|
192
|
+
|
|
193
|
+
DEBUG = False
|
|
194
|
+
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
|
|
195
|
+
|
|
196
|
+
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")
|
|
197
|
+
CSRF_TRUSTED_ORIGINS = [f"https://{{h}}" for h in ALLOWED_HOSTS]
|
|
198
|
+
WAGTAILADMIN_BASE_URL = f"https://{{ALLOWED_HOSTS[0]}}"
|
|
199
|
+
|
|
200
|
+
# TLS is terminated by the reverse proxy in front.
|
|
201
|
+
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
202
|
+
SESSION_COOKIE_SECURE = True
|
|
203
|
+
CSRF_COOKIE_SECURE = True
|
|
204
|
+
|
|
205
|
+
if DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3":
|
|
206
|
+
DATABASES["default"]["NAME"] = "{CONTAINER_DATA_DIR}/{DB_FILE}"
|
|
207
|
+
|
|
208
|
+
MEDIA_ROOT = "{CONTAINER_DATA_DIR}/{MEDIA_DIR}"
|
|
209
|
+
STATIC_ROOT = "{CONTAINER_DATA_DIR}/{STATIC_DIR}"
|
|
210
|
+
'''
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def dockerfile(python, deps, project, workers):
|
|
214
|
+
return f'''{MARKER}.
|
|
215
|
+
FROM {UV_IMAGE.format(python=python)}
|
|
216
|
+
|
|
217
|
+
WORKDIR {APP_DIR}
|
|
218
|
+
ENV UV_COMPILE_BYTECODE=1 \\
|
|
219
|
+
PYTHONDONTWRITEBYTECODE=1 \\
|
|
220
|
+
PYTHONUNBUFFERED=1 \\
|
|
221
|
+
VIRTUAL_ENV={APP_DIR}/.venv \\
|
|
222
|
+
PATH="{APP_DIR}/.venv/bin:$PATH" \\
|
|
223
|
+
DJANGO_SETTINGS_MODULE={SETTINGS_MODULE}
|
|
224
|
+
|
|
225
|
+
{deps}
|
|
226
|
+
RUN python -c "import granian" 2>/dev/null || uv pip install granian
|
|
227
|
+
|
|
228
|
+
COPY . .
|
|
229
|
+
COPY {GEN}/settings.py {SETTINGS_MODULE}.py
|
|
230
|
+
|
|
231
|
+
EXPOSE {APP_PORT}
|
|
232
|
+
CMD ["sh", "-c", "python manage.py migrate --noinput && python manage.py collectstatic --noinput && exec granian --interface wsgi {project}.wsgi:application --host 0.0.0.0 --port {APP_PORT} --workers {workers}"]
|
|
233
|
+
'''
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# BuildKit picks up <Dockerfile>.dockerignore next to the Dockerfile.
|
|
237
|
+
DOCKERIGNORE = f"""\
|
|
238
|
+
.git
|
|
239
|
+
.venv
|
|
240
|
+
**/__pycache__
|
|
241
|
+
*.sqlite3
|
|
242
|
+
*.sqlite3-*
|
|
243
|
+
*.sqlite3.*
|
|
244
|
+
{MEDIA_DIR}
|
|
245
|
+
{STATIC_DIR}
|
|
246
|
+
{DATA_DIR}
|
|
247
|
+
backups
|
|
248
|
+
node_modules
|
|
249
|
+
{ENV_FILE}
|
|
250
|
+
{COMPOSE_FILE}
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
CADDYFILE = f"""{MARKER}.
|
|
254
|
+
{{
|
|
255
|
+
admin off
|
|
256
|
+
# Pass through X-Forwarded-Proto from the host's reverse proxy.
|
|
257
|
+
servers {{
|
|
258
|
+
trusted_proxies static private_ranges
|
|
259
|
+
}}
|
|
260
|
+
}}
|
|
261
|
+
|
|
262
|
+
:80 {{
|
|
263
|
+
encode zstd gzip
|
|
264
|
+
|
|
265
|
+
handle_path /static/* {{
|
|
266
|
+
root * {CADDY_STATIC_DIR}
|
|
267
|
+
header Cache-Control "public, max-age=31536000, immutable"
|
|
268
|
+
file_server
|
|
269
|
+
}}
|
|
270
|
+
handle_path /media/* {{
|
|
271
|
+
root * {CADDY_MEDIA_DIR}
|
|
272
|
+
file_server
|
|
273
|
+
}}
|
|
274
|
+
handle {{
|
|
275
|
+
reverse_proxy web:{APP_PORT}
|
|
276
|
+
}}
|
|
277
|
+
}}
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def compose(port, caddy):
|
|
282
|
+
# Localhost only: Docker-published ports bypass host firewalls.
|
|
283
|
+
ports = f""" ports:
|
|
284
|
+
- "127.0.0.1:{port}:{{}}"
|
|
285
|
+
"""
|
|
286
|
+
out = f"""{MARKER}. Re-run build-wagtail to change this file; secrets and hosts are in {ENV_FILE}.
|
|
287
|
+
services:
|
|
288
|
+
web:
|
|
289
|
+
build:
|
|
290
|
+
context: .
|
|
291
|
+
dockerfile: {GEN}/Dockerfile
|
|
292
|
+
restart: unless-stopped
|
|
293
|
+
env_file: {ENV_FILE}
|
|
294
|
+
user: "${{PUID}}:${{PGID}}"
|
|
295
|
+
volumes:
|
|
296
|
+
- ./{DATA_DIR}:{CONTAINER_DATA_DIR}
|
|
297
|
+
"""
|
|
298
|
+
if not caddy:
|
|
299
|
+
return out + ports.format(APP_PORT)
|
|
300
|
+
|
|
301
|
+
return out + f"""
|
|
302
|
+
# Serves /static and /media, proxies the rest to granian.
|
|
303
|
+
caddy:
|
|
304
|
+
image: {CADDY_IMAGE}
|
|
305
|
+
restart: unless-stopped
|
|
306
|
+
depends_on: [web]
|
|
307
|
+
{ports.format(80)} volumes:
|
|
308
|
+
- ./{GEN}/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
309
|
+
- ./{DATA_DIR}/{STATIC_DIR}:{CADDY_STATIC_DIR}:ro
|
|
310
|
+
- ./{DATA_DIR}/{MEDIA_DIR}:{CADDY_MEDIA_DIR}:ro
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def proxy_example(host, port, caddy):
|
|
315
|
+
"""Return an example host Caddy config for the site."""
|
|
316
|
+
if caddy:
|
|
317
|
+
return f""" {host} {{
|
|
318
|
+
reverse_proxy 127.0.0.1:{port}
|
|
319
|
+
}}"""
|
|
320
|
+
|
|
321
|
+
data = DATA_DIR.resolve()
|
|
322
|
+
return f""" {host} {{
|
|
323
|
+
encode zstd gzip
|
|
324
|
+
|
|
325
|
+
handle_path /static/* {{
|
|
326
|
+
root * {data / STATIC_DIR}
|
|
327
|
+
header Cache-Control "public, max-age=31536000, immutable"
|
|
328
|
+
file_server
|
|
329
|
+
}}
|
|
330
|
+
handle_path /media/* {{
|
|
331
|
+
root * {data / MEDIA_DIR}
|
|
332
|
+
file_server
|
|
333
|
+
}}
|
|
334
|
+
handle {{
|
|
335
|
+
reverse_proxy 127.0.0.1:{port}
|
|
336
|
+
}}
|
|
337
|
+
}}"""
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def write_files(args, project, settings, python, deps):
|
|
341
|
+
GEN.mkdir(exist_ok=True)
|
|
342
|
+
(GEN / "settings.py").write_text(settings_py(settings))
|
|
343
|
+
(GEN / "Dockerfile").write_text(dockerfile(python, deps, project, args.workers))
|
|
344
|
+
(GEN / "Dockerfile.dockerignore").write_text(DOCKERIGNORE)
|
|
345
|
+
|
|
346
|
+
if args.caddy:
|
|
347
|
+
(GEN / "Caddyfile").write_text(CADDYFILE)
|
|
348
|
+
|
|
349
|
+
COMPOSE_FILE.write_text(compose(args.port, args.caddy))
|
|
350
|
+
print(f"Wrote {COMPOSE_FILE} and {GEN}/ (project: {project}, settings: {settings}, "
|
|
351
|
+
f"python: {python}, caddy: {'yes' if args.caddy else 'no'})")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main():
|
|
355
|
+
args = parse_args()
|
|
356
|
+
|
|
357
|
+
if not MANAGE_PY.is_file():
|
|
358
|
+
die(f"{MANAGE_PY} not found. Run this from the Wagtail project root.")
|
|
359
|
+
if not args.no_build and subprocess.run(["docker", "compose", "version"], capture_output=True).returncode != 0:
|
|
360
|
+
die("docker compose is not available.")
|
|
361
|
+
if COMPOSE_FILE.exists() and not COMPOSE_FILE.read_text().startswith(MARKER):
|
|
362
|
+
die(f"{COMPOSE_FILE} exists and wasn't generated by this script. Move it aside first.")
|
|
363
|
+
|
|
364
|
+
project, settings = find_settings(args.settings)
|
|
365
|
+
python = find_python(args.python)
|
|
366
|
+
deps = deps_step()
|
|
367
|
+
|
|
368
|
+
if not ENV_FILE.exists():
|
|
369
|
+
if not args.hosts:
|
|
370
|
+
die("first run needs the site's hostnames, e.g. build-wagtail example.org,www.example.org")
|
|
371
|
+
create_env(args.hosts)
|
|
372
|
+
elif args.hosts:
|
|
373
|
+
update_env_hosts(args.hosts)
|
|
374
|
+
env = read_env()
|
|
375
|
+
|
|
376
|
+
setup_data(int(env["PUID"]), int(env["PGID"]))
|
|
377
|
+
write_files(args, project, settings, python, deps)
|
|
378
|
+
|
|
379
|
+
if not args.no_build and subprocess.run(["docker", "compose", "build"]).returncode != 0:
|
|
380
|
+
die("docker compose build failed.")
|
|
381
|
+
|
|
382
|
+
host = env["DJANGO_ALLOWED_HOSTS"].split(",")[0]
|
|
383
|
+
print(f"""
|
|
384
|
+
Done. Start with: docker compose up -d
|
|
385
|
+
Stop with: docker compose down
|
|
386
|
+
Admin user: docker compose exec web python manage.py createsuperuser
|
|
387
|
+
|
|
388
|
+
The site is on 127.0.0.1:{args.port}. Point your reverse proxy at it for TLS, e.g. Caddy:
|
|
389
|
+
|
|
390
|
+
{proxy_example(host, args.port, args.caddy)}
|
|
391
|
+
|
|
392
|
+
{DATA_DIR}/ now holds the live database and media. Back it up.""")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
if __name__ == "__main__":
|
|
396
|
+
main()
|