djaploy 1.2.7__tar.gz → 1.2.9__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.
- djaploy-1.2.9/PKG-INFO +526 -0
- djaploy-1.2.9/README.md +464 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/systemd/infra/djaploy_hooks.py +5 -5
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/infra/djaploy_hooks.py +51 -15
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/infra/templates.py +4 -7
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/infra/utils.py +30 -0
- djaploy-1.2.9/djaploy/version.py +1 -0
- djaploy-1.2.9/djaploy.egg-info/PKG-INFO +526 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/pyproject.toml +1 -1
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_bluegreen.py +1 -1
- djaploy-1.2.7/PKG-INFO +0 -558
- djaploy-1.2.7/README.md +0 -496
- djaploy-1.2.7/djaploy/version.py +0 -1
- djaploy-1.2.7/djaploy.egg-info/PKG-INFO +0 -558
- {djaploy-1.2.7 → djaploy-1.2.9}/LICENSE +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/app.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/borg/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/borg/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/borg/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/janitor/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/janitor/apps.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/janitor/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/janitor/infra/commands/createjanitoruser.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/janitor/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/nginx/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/nginx/apps.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/nginx/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/nginx/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/rclone/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/rclone/apps.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/rclone/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/rclone/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/sync_certs/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/sync_certs/apps.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/sync_certs/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/sync_certs/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/systemd/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/systemd/apps.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/systemd/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/tailscale/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/tailscale/apps.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/tailscale/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/tailscale/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/versioning/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/versioning/apps.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/versioning/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/apps/versioning/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/artifact.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/bin/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/bin/django_pyinfra.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/bin/gunicornherder.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/builtin_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/certificates.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/changelog.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/_utils.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/activate.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/configure.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/deploy.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/restore.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/rollback.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/status.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/commands/sync_certs.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/config.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/deploy.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/discovery.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/infra/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/infra/bluegreen.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/commands/__init__.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/commands/djaploy.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/commands/restore_backup.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/commands/sync_certs.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/commands/update_certs.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/commands/verify.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/management/utils.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/notifications.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/utils.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy/versioning.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy.egg-info/SOURCES.txt +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy.egg-info/dependency_links.txt +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy.egg-info/entry_points.txt +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy.egg-info/requires.txt +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/djaploy.egg-info/top_level.txt +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/setup.cfg +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_borg.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_config.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_deploy_scripts.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_discovery.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_gunicornherder.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_hooks.py +0 -0
- {djaploy-1.2.7 → djaploy-1.2.9}/tests/test_versioning.py +0 -0
djaploy-1.2.9/PKG-INFO
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: djaploy
|
|
3
|
+
Version: 1.2.9
|
|
4
|
+
Summary: Modular Django deployment system based on pyinfra
|
|
5
|
+
Author-email: Johanna Mae Dimayuga <johanna@techco.fi>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Technology-Company
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/Technology-Company/djaploy
|
|
28
|
+
Project-URL: Repository, https://github.com/Technology-Company/djaploy
|
|
29
|
+
Project-URL: Issues, https://github.com/Technology-Company/djaploy/issues
|
|
30
|
+
Project-URL: Documentation, https://github.com/Technology-Company/djaploy#readme
|
|
31
|
+
Keywords: django,deployment,pyinfra,automation,infrastructure
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Framework :: Django
|
|
35
|
+
Classifier: Framework :: Django :: 3.2
|
|
36
|
+
Classifier: Framework :: Django :: 4.0
|
|
37
|
+
Classifier: Framework :: Django :: 4.1
|
|
38
|
+
Classifier: Framework :: Django :: 4.2
|
|
39
|
+
Classifier: Framework :: Django :: 5.0
|
|
40
|
+
Classifier: Intended Audience :: Developers
|
|
41
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
42
|
+
Classifier: Operating System :: OS Independent
|
|
43
|
+
Classifier: Programming Language :: Python :: 3
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
49
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
50
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
51
|
+
Classifier: Topic :: System :: Systems Administration
|
|
52
|
+
Requires-Python: >=3.9
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
License-File: LICENSE
|
|
55
|
+
Requires-Dist: pyinfra<3.8,>=3.4
|
|
56
|
+
Requires-Dist: django>=3.2
|
|
57
|
+
Provides-Extra: certificates
|
|
58
|
+
Requires-Dist: certbot>=2.0; extra == "certificates"
|
|
59
|
+
Provides-Extra: bunny
|
|
60
|
+
Requires-Dist: certbot-dns-bunny<=0.0.9; extra == "bunny"
|
|
61
|
+
Dynamic: license-file
|
|
62
|
+
|
|
63
|
+
# djaploy
|
|
64
|
+
|
|
65
|
+
[](https://pypi.org/project/djaploy/)
|
|
66
|
+
[](https://pypi.org/project/djaploy/)
|
|
67
|
+
[](https://github.com/Technology-Company/djaploy/blob/main/LICENSE)
|
|
68
|
+
[](https://github.com/Technology-Company/djaploy/commits)
|
|
69
|
+
|
|
70
|
+
A modular Django deployment system based on [pyinfra](https://pyinfra.com/), designed to standardize and simplify infrastructure management across Django projects.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **App-based, modular architecture** — deployment behaviour ships as Django apps you add to `INSTALLED_APPS`
|
|
75
|
+
- **Django integration** — drive everything through `manage.py` commands
|
|
76
|
+
- **Multiple deployment strategies** — `in_place`, `zero_downtime`, and `bluegreen`
|
|
77
|
+
- **Generated config** — systemd units and nginx sites are rendered from templates (no hand-maintained config files)
|
|
78
|
+
- **Generated local settings** — optionally write a `local.py` with production values on the server
|
|
79
|
+
- **Infrastructure as code** — define hosts in Python with pyinfra
|
|
80
|
+
- **Git-based artifacts** — automated artifact creation from your git repository
|
|
81
|
+
- **SSL management** — issue/renew certificates (Let's Encrypt, Bunny DNS, Tailscale) and sync them to servers
|
|
82
|
+
- **Release notifications & versioning** — semantic version tags, changelogs, and Slack/webhook notifications
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install djaploy
|
|
88
|
+
# or: poetry add djaploy
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Optional extras
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install djaploy[certificates] # Let's Encrypt / certbot support
|
|
95
|
+
pip install djaploy[bunny] # Bunny DNS certbot plugin
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Quick Start
|
|
99
|
+
|
|
100
|
+
### 1. Add djaploy to Django settings
|
|
101
|
+
|
|
102
|
+
Add the base `djaploy` app plus the feature apps you want. Each feature is its own Django app
|
|
103
|
+
that contributes deploy hooks when present in `INSTALLED_APPS`:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
INSTALLED_APPS = [
|
|
107
|
+
# ... your apps ...
|
|
108
|
+
"djaploy", # management commands + core deploy hooks (required)
|
|
109
|
+
"djaploy.apps.nginx", # generate + deploy nginx config, manage SSL, reload
|
|
110
|
+
"djaploy.apps.systemd", # reload systemd, manage services
|
|
111
|
+
"djaploy.apps.sync_certs", # sync certs from 1Password to servers
|
|
112
|
+
# Other available apps:
|
|
113
|
+
# "djaploy.apps.versioning", "djaploy.apps.borg", "djaploy.apps.rclone",
|
|
114
|
+
# "djaploy.apps.tailscale", "djaploy.apps.janitor",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
# Required paths (plain strings or Path objects both work)
|
|
118
|
+
import os
|
|
119
|
+
|
|
120
|
+
BASE_DIR = os.path.dirname(...) # your Django project dir (contains manage.py's package)
|
|
121
|
+
GIT_DIR = os.path.dirname(BASE_DIR) # repo root (where .git lives) — used for artifacts/versioning
|
|
122
|
+
# ARTIFACT_DIR = "deployment" # optional; where artifacts are written (default: "deployment")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> **Migrating from 0.x?** `DjaployConfig`, `module_configs`, `modules=[...]`, the `infra/config.py`
|
|
126
|
+
> file, and the `deploy_files/` copy mechanism have been removed. All deployment config now lives on
|
|
127
|
+
> `HostConfig`, features are enabled via `INSTALLED_APPS`, and systemd/nginx are generated from
|
|
128
|
+
> templates. See [Configuration](#configuration) below.
|
|
129
|
+
|
|
130
|
+
### 2. Create the project structure
|
|
131
|
+
|
|
132
|
+
djaploy discovers infrastructure by scanning each installed app's `infra/` directory (in
|
|
133
|
+
`INSTALLED_APPS` order, first match wins). Put your deployment config inside one of your Django apps:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
your_app/
|
|
137
|
+
├── infra/
|
|
138
|
+
│ ├── inventory/
|
|
139
|
+
│ │ ├── production.py # hosts = [HostConfig(...), ...]
|
|
140
|
+
│ │ └── staging.py
|
|
141
|
+
│ ├── certificates.py # all_certificates = [...] (optional, for SSL)
|
|
142
|
+
│ ├── prepare.py # optional local pre-deploy build steps
|
|
143
|
+
│ └── djaploy_hooks.py # optional project-specific @deploy_hook functions
|
|
144
|
+
└── ...
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
There is **no** `infra/config.py` — host and deployment settings live entirely on `HostConfig`.
|
|
148
|
+
|
|
149
|
+
### 3. Define inventory
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# your_app/infra/inventory/production.py
|
|
153
|
+
from djaploy.config import HostConfig
|
|
154
|
+
|
|
155
|
+
hosts = [
|
|
156
|
+
HostConfig(
|
|
157
|
+
"web-1",
|
|
158
|
+
ssh_hostname="192.168.1.100",
|
|
159
|
+
ssh_user="deploy",
|
|
160
|
+
app_name="myapp", # deployment name == your Django package (see note below)
|
|
161
|
+
app_user="myapp",
|
|
162
|
+
deployment_strategy="zero_downtime",
|
|
163
|
+
python_version="3.11",
|
|
164
|
+
manage_py_path="manage.py", # relative path to manage.py inside the artifact
|
|
165
|
+
services=["myapp"],
|
|
166
|
+
gunicorn_conf={"workers": 3, "timeout": 30},
|
|
167
|
+
nginx_conf={"client_max_body_size": "25M"},
|
|
168
|
+
),
|
|
169
|
+
]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> **`app_name` and your Django package.** `app_name` drives the server app dir
|
|
173
|
+
> (`/home/{app_user}/apps/{app_name}`), the systemd service/socket names, and the nginx upstream.
|
|
174
|
+
> If you use [`generate_local_settings`](#generated-local-settings), `app_name` must match your
|
|
175
|
+
> Django package name, since the generated `local.py` is written to
|
|
176
|
+
> `{manage_subdir}/{app_name}/settings/local.py`.
|
|
177
|
+
|
|
178
|
+
### 4. Configure and deploy
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python manage.py djaploy configure --env production # one-time server setup
|
|
182
|
+
python manage.py djaploy deploy --env production # deploy latest git HEAD
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Configuration
|
|
186
|
+
|
|
187
|
+
All deployment configuration lives on `djaploy.config.HostConfig`. Commonly used fields:
|
|
188
|
+
|
|
189
|
+
| Field | Default | Purpose |
|
|
190
|
+
|-------|---------|---------|
|
|
191
|
+
| `ssh_hostname` | — (required) | SSH host |
|
|
192
|
+
| `ssh_user` / `ssh_port` / `ssh_key` | `deploy` / `22` / — | SSH connection |
|
|
193
|
+
| `ssh_known_hosts_file` | — | known_hosts for strict host verification |
|
|
194
|
+
| `app_name` | — (required) | Deployment name; drives dir/service/socket/nginx names |
|
|
195
|
+
| `app_user` | `app` | OS user the app runs as |
|
|
196
|
+
| `app_hostname` | — | Public hostname (used for `server_name` / ALLOWED_HOSTS) |
|
|
197
|
+
| `deployment_strategy` | `zero_downtime` | `in_place`, `zero_downtime`, or `bluegreen` |
|
|
198
|
+
| `python_version` / `python_compile` | `3.11` / `False` | Python on the server (apt or compiled) |
|
|
199
|
+
| `manage_py_path` | `manage.py` | Path to `manage.py` within the artifact |
|
|
200
|
+
| `services` / `timer_services` | — | systemd services/timers to manage |
|
|
201
|
+
| `domains` | — | Certificates/domains for SSL (see [Certificates](#certificate-management)) |
|
|
202
|
+
| `keep_releases` | `5` | Releases retained (zero_downtime) |
|
|
203
|
+
| `generate_local_settings` | `False` | Write `local.py` on the server (see below) |
|
|
204
|
+
| `shared_resources` | — | Extra paths symlinked from `shared/` |
|
|
205
|
+
| `db_dir` | — | External database directory template |
|
|
206
|
+
| `gunicorn_conf` | — | `workers`, `timeout`, `umask`, `wsgi_module`, `health_check_*` |
|
|
207
|
+
| `nginx_conf` | — | `server_name`, `listen`, `client_max_body_size`, `custom` |
|
|
208
|
+
| `core_conf` | — | `poetry_no_root`, `exclude_groups`, `poetry_lock`, `databases` |
|
|
209
|
+
| `versioning_conf` / `notifications_conf` | — | See [Release Notifications & Versioning](#release-notifications--versioning) |
|
|
210
|
+
| `backup` / `borg_backup` | — | `BackupConfig` / `BorgBackupConfig` |
|
|
211
|
+
|
|
212
|
+
## Deployment Strategies
|
|
213
|
+
|
|
214
|
+
djaploy supports three deployment strategies, configured via `deployment_strategy` on `HostConfig`.
|
|
215
|
+
|
|
216
|
+
### In-place (`"in_place"`)
|
|
217
|
+
|
|
218
|
+
The simplest strategy. Code is extracted directly into the app directory and services are restarted. Has brief downtime during restart.
|
|
219
|
+
|
|
220
|
+
### Zero-downtime (`"zero_downtime"`)
|
|
221
|
+
|
|
222
|
+
Uses a `releases/` directory with a `current` symlink. Each deploy creates a new immutable release, swaps the symlink atomically, and sends USR2 via gunicornherder to reload gunicorn. No downtime, but no pre-activation testing.
|
|
223
|
+
|
|
224
|
+
### Blue-green (`"bluegreen"`)
|
|
225
|
+
|
|
226
|
+
Two independent slots (blue and green), each running its own gunicorn process on a separate Unix socket. Traffic switching happens via nginx reload. Supports staging a release for testing before switching.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
HostConfig(
|
|
230
|
+
"my-server",
|
|
231
|
+
ssh_hostname="192.168.1.100",
|
|
232
|
+
app_name="myapp",
|
|
233
|
+
app_user="myapp-api",
|
|
234
|
+
deployment_strategy="bluegreen",
|
|
235
|
+
# ...
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Blue-green commands
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# Deploy to inactive slot (does NOT switch traffic)
|
|
243
|
+
python manage.py djaploy deploy --env production --latest
|
|
244
|
+
|
|
245
|
+
# Activate: switch nginx to the staged slot (zero downtime)
|
|
246
|
+
python manage.py djaploy activate --env production
|
|
247
|
+
|
|
248
|
+
# Deploy + activate in one step
|
|
249
|
+
python manage.py djaploy deploy --env production --latest --activate
|
|
250
|
+
|
|
251
|
+
# Show both slots with release info, paths, service status
|
|
252
|
+
python manage.py djaploy status --env production
|
|
253
|
+
|
|
254
|
+
# Rollback: switch back to previous slot (instant)
|
|
255
|
+
python manage.py djaploy rollback --env production
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
> **Note:** Migrations run during deploy, before traffic switches. Both slots share the same
|
|
259
|
+
> database, so migrations must be **backward-compatible** (expand/contract pattern).
|
|
260
|
+
|
|
261
|
+
### Server directory layout comparison
|
|
262
|
+
|
|
263
|
+
For `app_user="myapp-api"`, `app_name="myapp"`:
|
|
264
|
+
|
|
265
|
+
| Path | `in_place` | `zero_downtime` | `bluegreen` |
|
|
266
|
+
|------|-----------|-----------------|-------------|
|
|
267
|
+
| App code | `.../apps/myapp/` | `.../apps/myapp/current/` | `.../apps/myapp/slots/{blue\|green}/` |
|
|
268
|
+
| Virtualenv | Managed by Poetry | `.../shared/venv-{HASH}-py{ver}/` | `.../shared/venv-{HASH}-py{ver}/` |
|
|
269
|
+
| Static files | `.../apps/myapp/static/` | `.../apps/myapp/shared/static/` | `.../apps/myapp/shared/static/` |
|
|
270
|
+
| Media files | `.../apps/myapp/media/` | `.../apps/myapp/shared/media/` | `.../apps/myapp/shared/media/` |
|
|
271
|
+
|
|
272
|
+
All paths are relative to `/home/{app_user}/`.
|
|
273
|
+
|
|
274
|
+
#### Systemd services comparison
|
|
275
|
+
|
|
276
|
+
| Strategy | Service name | Socket path | Process |
|
|
277
|
+
|----------|-------------|-------------|---------|
|
|
278
|
+
| `in_place` | `{app}.service` | `/run/{app}/{app}.sock` | `poetry run gunicorn` |
|
|
279
|
+
| `zero_downtime` | `{app}.service` | `/run/{app}/{app}.sock` | gunicornherder wrapping gunicorn |
|
|
280
|
+
| `bluegreen` | `{app}-blue.service`, `{app}-green.service` | `/run/{app}-{slot}/{app}.sock` | gunicorn (`Type=notify`) |
|
|
281
|
+
|
|
282
|
+
## Generated configuration
|
|
283
|
+
|
|
284
|
+
In 1.x, djaploy **generates** systemd units and nginx sites from templates
|
|
285
|
+
(`djaploy/infra/templates.py`) and writes them to the server during `deploy`/`configure` — there is
|
|
286
|
+
no `deploy_files/` directory to maintain.
|
|
287
|
+
|
|
288
|
+
### systemd
|
|
289
|
+
|
|
290
|
+
A unit is rendered for the host's strategy (`SYSTEMD_IN_PLACE`, `SYSTEMD_ZERO_DOWNTIME`, or a
|
|
291
|
+
per-slot `SYSTEMD_BLUEGREEN`) to `/etc/systemd/system/{app_name}.service`. Workers, timeout, umask,
|
|
292
|
+
and the WSGI module come from `gunicorn_conf` (the WSGI module otherwise derives from Django's
|
|
293
|
+
`WSGI_APPLICATION`, falling back to `{app_name}.wsgi:application`).
|
|
294
|
+
|
|
295
|
+
### nginx
|
|
296
|
+
|
|
297
|
+
The `djaploy.apps.nginx` app installs nginx, deploys SSL certs, symlinks the site, and reloads.
|
|
298
|
+
The site config is rendered from:
|
|
299
|
+
|
|
300
|
+
- `NGINX_SITE` / `NGINX_SITE_SSL` for `in_place` / `zero_downtime`
|
|
301
|
+
- `NGINX_SITE_BLUEGREEN` / `NGINX_SITE_SSL_BLUEGREEN` (+ a separate upstream file rewritten on
|
|
302
|
+
activation) for `bluegreen`
|
|
303
|
+
|
|
304
|
+
The SSL variants are selected automatically when the host has `domains` with certificates. Template
|
|
305
|
+
values are derived from `HostConfig`:
|
|
306
|
+
|
|
307
|
+
- `server_name` — `nginx_conf["server_name"]`, else the first domain's identifier, else `app_hostname`, else `_`
|
|
308
|
+
- `ssl_certificate` / `ssl_certificate_key` — `/home/{app_user}/.ssl/{identifier}.{crt,key}`
|
|
309
|
+
- static/media aliases — `{app_path}/static` and `{app_path}/media` (or `shared/...` for zero_downtime/bluegreen), overridable via `nginx_conf["static_path"]` / `nginx_conf["media_path"]`
|
|
310
|
+
- `client_max_body_size` — `nginx_conf["client_max_body_size"]` (default `10M`), `listen` — `nginx_conf["listen"]`
|
|
311
|
+
|
|
312
|
+
**Custom static/media locations:** set `nginx_conf={"static_path": ..., "media_path": ...}` to point
|
|
313
|
+
nginx (and, for zero_downtime/bluegreen, the generated `local.py` `STATIC_ROOT`/`MEDIA_ROOT`) at a
|
|
314
|
+
custom directory. Each value may be absolute (leading `/`) or relative to `{app_path}`. Make sure
|
|
315
|
+
your Django `STATIC_ROOT`/`MEDIA_ROOT` resolve to the same paths. Example — serve from a `public/`
|
|
316
|
+
dir next to `manage.py`:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
nginx_conf={
|
|
320
|
+
"static_path": "myproject/public/static", # -> {app_path}/myproject/public/static
|
|
321
|
+
"media_path": "myproject/public/media",
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Bring your own nginx:** set `nginx_conf={"custom": True}` to skip built-in nginx generation and
|
|
326
|
+
manage the config yourself (e.g. via a custom `deploy:configure` / `activate:post` hook).
|
|
327
|
+
|
|
328
|
+
### Generated local settings
|
|
329
|
+
|
|
330
|
+
Set `generate_local_settings=True` to have djaploy write
|
|
331
|
+
`{manage_subdir}/{app_name}/settings/local.py` on the server during deploy, containing `DEBUG=False`,
|
|
332
|
+
`ALLOWED_HOSTS` (from `app_hostname`), `DATABASES` (when `db_dir` is set), and — for
|
|
333
|
+
`zero_downtime`/`bluegreen` — `STATIC_ROOT`/`MEDIA_ROOT`. Your project settings must import it:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
try:
|
|
337
|
+
from .local import * # noqa
|
|
338
|
+
except ImportError:
|
|
339
|
+
pass
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Because the path is keyed on `app_name`, `app_name` must equal your Django settings package name.
|
|
343
|
+
|
|
344
|
+
## Commands
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Deployment lifecycle
|
|
348
|
+
python manage.py djaploy deploy --env <env> [--local | --latest | --release TAG] [--activate]
|
|
349
|
+
python manage.py djaploy configure --env <env>
|
|
350
|
+
python manage.py djaploy rollback --env <env> [--release NAME]
|
|
351
|
+
python manage.py djaploy activate --env <env> # bluegreen
|
|
352
|
+
python manage.py djaploy status --env <env> # bluegreen
|
|
353
|
+
python manage.py djaploy --list # list available commands
|
|
354
|
+
|
|
355
|
+
# Certificates
|
|
356
|
+
python manage.py update_certs --email admin@example.com [--staging] [--force]
|
|
357
|
+
python manage.py sync_certs --env <env>
|
|
358
|
+
|
|
359
|
+
# Diagnostics / backups
|
|
360
|
+
python manage.py verify --verbose
|
|
361
|
+
python manage.py restore_backup --env <env>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Deploy modes: `--local` (uncommitted working tree), `--latest` (git HEAD, default), `--release TAG`.
|
|
365
|
+
Version bumps: `--bump-major | --bump-minor | --bump-patch`.
|
|
366
|
+
|
|
367
|
+
## Certificate management
|
|
368
|
+
|
|
369
|
+
Define certificates in `<app>/infra/certificates.py`:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from djaploy.certificates import BunnyDnsCertificate, LetsEncryptCertificate, TailscaleDnsCertificate
|
|
373
|
+
|
|
374
|
+
all_certificates = [
|
|
375
|
+
prod_cert := BunnyDnsCertificate(
|
|
376
|
+
"example.com", "www.example.com",
|
|
377
|
+
op_crt="/MyProject/example.com/fullchain.pem", # 1Password item field for the cert
|
|
378
|
+
op_key="/MyProject/example.com/privkey.pem", # 1Password item field for the key
|
|
379
|
+
bunny_api_key_secret="/MyProject/Bunny - API Key/credential",
|
|
380
|
+
),
|
|
381
|
+
]
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Reference certificates from a host via `domains=[prod_cert]`. Then:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
python manage.py update_certs --email admin@example.com # issue/renew (to 1Password)
|
|
388
|
+
python manage.py sync_certs --env production # push certs to /home/{app_user}/.ssl/
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`update_certs` discovers `certificates.py` via app discovery and uses `settings.OP_ACCOUNT` for the
|
|
392
|
+
1Password account. Other certificate types: `LetsEncryptCertificate` (HTTP-01, optionally via an
|
|
393
|
+
SSH `SshHttpHook`) and `TailscaleDnsCertificate`.
|
|
394
|
+
|
|
395
|
+
## Project customization
|
|
396
|
+
|
|
397
|
+
### Hooks
|
|
398
|
+
|
|
399
|
+
Add `<app>/infra/djaploy_hooks.py` with `@deploy_hook(<phase>)` functions. They're auto-discovered
|
|
400
|
+
and run at the matching lifecycle phase. Remote (`deploy:*`) hooks receive `(host_data, artifact_path)`:
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
from djaploy.hooks import deploy_hook
|
|
404
|
+
|
|
405
|
+
@deploy_hook("deploy:configure")
|
|
406
|
+
def my_step(host_data, artifact_path):
|
|
407
|
+
from pyinfra.operations import server
|
|
408
|
+
server.shell(name="example", commands=["echo hello"], _sudo=True)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Phases (in order): `configure`, then per-deploy `deploy:upload` → `deploy:configure` → `deploy:pre`
|
|
412
|
+
→ `deploy:start`; plus `activate`/`rollback` (and their `:pre`/`:post`) for those commands. The
|
|
413
|
+
management command also runs `{command}:precommand` / `precommand` / `{command}:postcommand` /
|
|
414
|
+
`postcommand` locally around the pyinfra run.
|
|
415
|
+
|
|
416
|
+
### prepare.py
|
|
417
|
+
|
|
418
|
+
Add `<app>/infra/prepare.py` for local build steps run before the artifact is created (skipped with
|
|
419
|
+
`--skip-prepare`):
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
from pyinfra import local
|
|
423
|
+
local.shell("npm run build")
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Release Notifications & Versioning
|
|
427
|
+
|
|
428
|
+
djaploy includes built-in support for semantic versioning, changelog generation, and deployment notifications. When enabled, deployments automatically:
|
|
429
|
+
|
|
430
|
+
- Calculate the next semantic version based on git tags
|
|
431
|
+
- Generate a changelog from commit messages (simple or AI-powered)
|
|
432
|
+
- Send notifications to Slack or custom webhooks
|
|
433
|
+
- Create and push git tags after successful deployments
|
|
434
|
+
- Deploy a `VERSION` file to the server
|
|
435
|
+
|
|
436
|
+
### Enabling the feature
|
|
437
|
+
|
|
438
|
+
Configure `versioning_conf` and `notifications_conf` on your `HostConfig` (requires
|
|
439
|
+
`djaploy.apps.versioning` in `INSTALLED_APPS`):
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
from djaploy.config import HostConfig
|
|
443
|
+
|
|
444
|
+
hosts = [
|
|
445
|
+
HostConfig(
|
|
446
|
+
"web-1",
|
|
447
|
+
ssh_hostname="192.168.1.100",
|
|
448
|
+
app_name="myapp",
|
|
449
|
+
# ...
|
|
450
|
+
versioning_conf={
|
|
451
|
+
"tag_environments": ["production"], # Create tags only for these envs
|
|
452
|
+
"increment_type": "patch", # Default: patch (v1.0.0 -> v1.0.1)
|
|
453
|
+
"push_tags": True, # Push tags to remote
|
|
454
|
+
},
|
|
455
|
+
notifications_conf={
|
|
456
|
+
"display_name": "My App",
|
|
457
|
+
"notify": True,
|
|
458
|
+
"notify_on_failure": True,
|
|
459
|
+
"webhook_url": "op://vault/slack/webhook-url",
|
|
460
|
+
"changelog_generator": "llm", # "simple" or "llm"
|
|
461
|
+
"changelog_config": {
|
|
462
|
+
"api_key": "op://vault/mistral/api-key",
|
|
463
|
+
"model": "devstral-small-latest",
|
|
464
|
+
"api_url": "https://api.mistral.ai/v1/chat/completions",
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
),
|
|
468
|
+
]
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Configuration options
|
|
472
|
+
|
|
473
|
+
**Versioning (`versioning_conf`)**
|
|
474
|
+
|
|
475
|
+
| Option | Default | Description |
|
|
476
|
+
|--------|---------|-------------|
|
|
477
|
+
| `tag_environments` | `["production"]` | Environments that create git tags |
|
|
478
|
+
| `increment_type` | `"patch"` | Default version bump: `major`, `minor`, or `patch` |
|
|
479
|
+
| `push_tags` | `True` | Push created tags to remote |
|
|
480
|
+
| `version_file_path` | `"VERSION"` | Path for VERSION file on server |
|
|
481
|
+
|
|
482
|
+
**Notifications (`notifications_conf`)**
|
|
483
|
+
|
|
484
|
+
| Option | Default | Description |
|
|
485
|
+
|--------|---------|-------------|
|
|
486
|
+
| `display_name` | `app_name` | Name shown in notification messages |
|
|
487
|
+
| `notify` | `False` | Enable notifications for this environment |
|
|
488
|
+
| `notify_on_failure` | `True` | Send notification on deployment failure |
|
|
489
|
+
| `webhook_url` | — | Slack webhook URL (required) |
|
|
490
|
+
| `changelog_generator` | `"simple"` | Generator type: `simple` or `llm` |
|
|
491
|
+
| `changelog_config` | `{}` | Config passed to changelog generator |
|
|
492
|
+
|
|
493
|
+
### Version bump override
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
python manage.py djaploy deploy --env production --bump-minor # v1.0.0 -> v1.1.0
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### VERSION file
|
|
500
|
+
|
|
501
|
+
The versioning app deploys a `VERSION` file to the server:
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
VERSION=v1.0.5
|
|
505
|
+
COMMIT=abc1234
|
|
506
|
+
DEPLOYED_AT=2024-01-15T10:30:00Z
|
|
507
|
+
ENVIRONMENT=production
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Development
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
git clone https://github.com/Technology-Company/djaploy.git
|
|
514
|
+
cd djaploy
|
|
515
|
+
poetry install
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
To use a local development copy in another project:
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
pip install -e /path/to/djaploy
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
## License
|
|
525
|
+
|
|
526
|
+
[MIT](LICENSE)
|