djaploy 1.2.8__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.
Files changed (95) hide show
  1. djaploy-1.2.9/PKG-INFO +526 -0
  2. djaploy-1.2.9/README.md +464 -0
  3. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/infra/djaploy_hooks.py +6 -4
  4. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/infra/templates.py +4 -7
  5. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/infra/utils.py +30 -0
  6. djaploy-1.2.9/djaploy/version.py +1 -0
  7. djaploy-1.2.9/djaploy.egg-info/PKG-INFO +526 -0
  8. {djaploy-1.2.8 → djaploy-1.2.9}/pyproject.toml +1 -1
  9. djaploy-1.2.8/PKG-INFO +0 -558
  10. djaploy-1.2.8/README.md +0 -496
  11. djaploy-1.2.8/djaploy/version.py +0 -1
  12. djaploy-1.2.8/djaploy.egg-info/PKG-INFO +0 -558
  13. {djaploy-1.2.8 → djaploy-1.2.9}/LICENSE +0 -0
  14. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/__init__.py +0 -0
  15. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/app.py +0 -0
  16. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/__init__.py +0 -0
  17. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/borg/__init__.py +0 -0
  18. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/borg/infra/__init__.py +0 -0
  19. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/borg/infra/djaploy_hooks.py +0 -0
  20. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/janitor/__init__.py +0 -0
  21. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/janitor/apps.py +0 -0
  22. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/janitor/infra/__init__.py +0 -0
  23. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/janitor/infra/commands/createjanitoruser.py +0 -0
  24. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/janitor/infra/djaploy_hooks.py +0 -0
  25. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/nginx/__init__.py +0 -0
  26. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/nginx/apps.py +0 -0
  27. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/nginx/infra/__init__.py +0 -0
  28. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/nginx/infra/djaploy_hooks.py +0 -0
  29. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/rclone/__init__.py +0 -0
  30. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/rclone/apps.py +0 -0
  31. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/rclone/infra/__init__.py +0 -0
  32. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/rclone/infra/djaploy_hooks.py +0 -0
  33. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/sync_certs/__init__.py +0 -0
  34. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/sync_certs/apps.py +0 -0
  35. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/sync_certs/infra/__init__.py +0 -0
  36. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/sync_certs/infra/djaploy_hooks.py +0 -0
  37. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/systemd/__init__.py +0 -0
  38. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/systemd/apps.py +0 -0
  39. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/systemd/infra/__init__.py +0 -0
  40. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/systemd/infra/djaploy_hooks.py +0 -0
  41. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/tailscale/__init__.py +0 -0
  42. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/tailscale/apps.py +0 -0
  43. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/tailscale/infra/__init__.py +0 -0
  44. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/tailscale/infra/djaploy_hooks.py +0 -0
  45. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/versioning/__init__.py +0 -0
  46. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/versioning/apps.py +0 -0
  47. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/versioning/infra/__init__.py +0 -0
  48. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/apps/versioning/infra/djaploy_hooks.py +0 -0
  49. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/artifact.py +0 -0
  50. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/bin/__init__.py +0 -0
  51. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/bin/django_pyinfra.py +0 -0
  52. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/bin/gunicornherder.py +0 -0
  53. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/builtin_hooks.py +0 -0
  54. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/certificates.py +0 -0
  55. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/changelog.py +0 -0
  56. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/__init__.py +0 -0
  57. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/_utils.py +0 -0
  58. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/activate.py +0 -0
  59. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/configure.py +0 -0
  60. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/deploy.py +0 -0
  61. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/restore.py +0 -0
  62. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/rollback.py +0 -0
  63. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/status.py +0 -0
  64. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/commands/sync_certs.py +0 -0
  65. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/config.py +0 -0
  66. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/deploy.py +0 -0
  67. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/discovery.py +0 -0
  68. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/hooks.py +0 -0
  69. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/infra/__init__.py +0 -0
  70. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/infra/bluegreen.py +0 -0
  71. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/__init__.py +0 -0
  72. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/commands/__init__.py +0 -0
  73. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/commands/djaploy.py +0 -0
  74. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/commands/restore_backup.py +0 -0
  75. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/commands/sync_certs.py +0 -0
  76. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/commands/update_certs.py +0 -0
  77. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/commands/verify.py +0 -0
  78. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/management/utils.py +0 -0
  79. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/notifications.py +0 -0
  80. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/utils.py +0 -0
  81. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy/versioning.py +0 -0
  82. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy.egg-info/SOURCES.txt +0 -0
  83. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy.egg-info/dependency_links.txt +0 -0
  84. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy.egg-info/entry_points.txt +0 -0
  85. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy.egg-info/requires.txt +0 -0
  86. {djaploy-1.2.8 → djaploy-1.2.9}/djaploy.egg-info/top_level.txt +0 -0
  87. {djaploy-1.2.8 → djaploy-1.2.9}/setup.cfg +0 -0
  88. {djaploy-1.2.8 → djaploy-1.2.9}/tests/test_bluegreen.py +0 -0
  89. {djaploy-1.2.8 → djaploy-1.2.9}/tests/test_borg.py +0 -0
  90. {djaploy-1.2.8 → djaploy-1.2.9}/tests/test_config.py +0 -0
  91. {djaploy-1.2.8 → djaploy-1.2.9}/tests/test_deploy_scripts.py +0 -0
  92. {djaploy-1.2.8 → djaploy-1.2.9}/tests/test_discovery.py +0 -0
  93. {djaploy-1.2.8 → djaploy-1.2.9}/tests/test_gunicornherder.py +0 -0
  94. {djaploy-1.2.8 → djaploy-1.2.9}/tests/test_hooks.py +0 -0
  95. {djaploy-1.2.8 → 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
+ [![PyPI Version](https://badgen.net/pypi/v/djaploy)](https://pypi.org/project/djaploy/)
66
+ [![Python Versions](https://badgen.net/pypi/python/djaploy)](https://pypi.org/project/djaploy/)
67
+ [![License](https://badgen.net/badge/license/MIT/blue)](https://github.com/Technology-Company/djaploy/blob/main/LICENSE)
68
+ [![Last Commit](https://badgen.net/github/last-commit/Technology-Company/djaploy)](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)