@django-stack/cli 0.8.0 → 0.9.0
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.
- package/dist/bin.js +814 -53
- package/dist/index.d.ts +32 -1
- package/dist/index.js +1 -1
- package/dist/{production-CGkI7oNe.js → production-M2SsuuBb.js} +61 -3
- package/package.json +1 -1
- package/templates/backend/Dockerfile +4 -5
- package/templates/backend/config/settings.py +2 -0
- package/templates/backend/config/studio.py +4 -0
- package/templates/backend/content/apps.py +5 -1
- package/templates/backend/content/notifications.py +36 -0
- package/templates/backend/content/tests/test_api.py +18 -0
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/Caddyfile +7 -1
- package/templates/base/compose.yaml +30 -2
- package/templates/base/env.example +17 -0
- package/templates/frontend-astro/Dockerfile +6 -3
- package/templates/frontend-astro/package.json +1 -1
- package/templates/frontend-next/Dockerfile +2 -2
- package/templates/frontend-next/package.json +3 -3
- package/templates/frontend-nuxt/Dockerfile +2 -2
- package/templates/frontend-nuxt/package.json +2 -2
- package/templates/frontend-sveltekit/Dockerfile +2 -2
- package/templates/frontend-sveltekit/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -158,7 +158,7 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
158
158
|
readonly value: "on";
|
|
159
159
|
readonly label: "On";
|
|
160
160
|
readonly hint: "in the admin and by email";
|
|
161
|
-
readonly generated:
|
|
161
|
+
readonly generated: true;
|
|
162
162
|
}, {
|
|
163
163
|
readonly value: "off";
|
|
164
164
|
readonly label: "Off";
|
|
@@ -233,6 +233,28 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
233
233
|
readonly hint: "any S3-compatible service; credentials later";
|
|
234
234
|
readonly generated: true;
|
|
235
235
|
}];
|
|
236
|
+
}, {
|
|
237
|
+
readonly key: "errors";
|
|
238
|
+
readonly flag: "--errors";
|
|
239
|
+
readonly message: "Error reports";
|
|
240
|
+
readonly description: "How you hear about errors in production";
|
|
241
|
+
readonly bits: 2;
|
|
242
|
+
readonly values: readonly [{
|
|
243
|
+
readonly value: "email";
|
|
244
|
+
readonly label: "Email";
|
|
245
|
+
readonly hint: "to the superusers, and in the admin";
|
|
246
|
+
readonly generated: true;
|
|
247
|
+
}, {
|
|
248
|
+
readonly value: "sentry";
|
|
249
|
+
readonly label: "Sentry";
|
|
250
|
+
readonly hint: "or GlitchTip, with SENTRY_DSN";
|
|
251
|
+
readonly generated: false;
|
|
252
|
+
}, {
|
|
253
|
+
readonly value: "off";
|
|
254
|
+
readonly label: "Off";
|
|
255
|
+
readonly hint: "the logs only";
|
|
256
|
+
readonly generated: true;
|
|
257
|
+
}];
|
|
236
258
|
}];
|
|
237
259
|
type Options = (typeof STACK_OPTIONS)[number];
|
|
238
260
|
type StackKey = Options["key"];
|
|
@@ -272,6 +294,10 @@ interface ScaffoldOptions {
|
|
|
272
294
|
localS3?: boolean;
|
|
273
295
|
/** Page analytics with the bundled Umami (the `analytics` profile). */
|
|
274
296
|
analytics?: boolean;
|
|
297
|
+
/** Content Studio's notifications app; on by default. */
|
|
298
|
+
notifications?: boolean;
|
|
299
|
+
/** How errors are reported in production; by email by default. */
|
|
300
|
+
errors?: Stack["errors"];
|
|
275
301
|
/** The cache backend; the database by default. */
|
|
276
302
|
cache?: Stack["cache"];
|
|
277
303
|
/** The task queue; Huey where the cache is by default. */
|
|
@@ -295,6 +321,11 @@ interface ScaffoldResult {
|
|
|
295
321
|
}
|
|
296
322
|
/** The `.env` values of a cache and a task queue: CACHE_URL, TASKS_URL and TASK_WORKERS. */
|
|
297
323
|
export declare function cacheEnv(stack: Pick<Stack, "cache" | "tasks">): Record<string, string>;
|
|
324
|
+
/**
|
|
325
|
+
* Adds or removes Content Studio's notifications app in the text of
|
|
326
|
+
* `backend/config/settings.py`. Throws when it can't find where it goes.
|
|
327
|
+
*/
|
|
328
|
+
export declare function setNotificationsApp(settingsText: string, on: boolean): string;
|
|
298
329
|
/** Whether `.env` values use the Valkey service (the `valkey` profile). */
|
|
299
330
|
export declare function usesValkey(env: Record<string, string | undefined>): boolean;
|
|
300
331
|
/** A valid project name from a directory name: `My Site` becomes `my-site`. */
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { F as DEFAULT_STACK, i as FRONTENDS, I as NAMED_PRESETS, R as STACK_OPTIONS, y as VERSION, o as cacheEnv, B as decodePreset, V as encodePreset, x as generateProjectSecrets, S as generateSecret, l as normalizeDatabaseUrl, tt as parseEnv, H as pendingChoices, n as productionEnv, U as resolvePreset, X as runDoctor, d as scaffold, rt as setEnvValues, f as setNotificationsApp, m as toProjectName, g as usesValkey, r as validateDomain, _ as validateProjectName } from "./production-M2SsuuBb.js";
|
|
@@ -582,7 +582,7 @@ const STACK_OPTIONS = [
|
|
|
582
582
|
value: "on",
|
|
583
583
|
label: "On",
|
|
584
584
|
hint: "in the admin and by email",
|
|
585
|
-
generated:
|
|
585
|
+
generated: true
|
|
586
586
|
}, {
|
|
587
587
|
value: "off",
|
|
588
588
|
label: "Off",
|
|
@@ -665,6 +665,33 @@ const STACK_OPTIONS = [
|
|
|
665
665
|
hint: "any S3-compatible service; credentials later",
|
|
666
666
|
generated: true
|
|
667
667
|
}]
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
key: "errors",
|
|
671
|
+
flag: "--errors",
|
|
672
|
+
message: "Error reports",
|
|
673
|
+
description: "How you hear about errors in production",
|
|
674
|
+
bits: 2,
|
|
675
|
+
values: [
|
|
676
|
+
{
|
|
677
|
+
value: "email",
|
|
678
|
+
label: "Email",
|
|
679
|
+
hint: "to the superusers, and in the admin",
|
|
680
|
+
generated: true
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
value: "sentry",
|
|
684
|
+
label: "Sentry",
|
|
685
|
+
hint: "or GlitchTip, with SENTRY_DSN",
|
|
686
|
+
generated: false
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
value: "off",
|
|
690
|
+
label: "Off",
|
|
691
|
+
hint: "the logs only",
|
|
692
|
+
generated: true
|
|
693
|
+
}
|
|
694
|
+
]
|
|
668
695
|
}
|
|
669
696
|
];
|
|
670
697
|
/**
|
|
@@ -945,7 +972,8 @@ function projectManifest(project) {
|
|
|
945
972
|
function projectStack(project) {
|
|
946
973
|
const fromEnv = {
|
|
947
974
|
analytics: project.profiles.includes("analytics") ? "umami" : "off",
|
|
948
|
-
...project.env.CACHE_URL === void 0 ? {} : cacheStack(project.env)
|
|
975
|
+
...project.env.CACHE_URL === void 0 ? {} : cacheStack(project.env),
|
|
976
|
+
...notificationsStack(project)
|
|
949
977
|
};
|
|
950
978
|
const { preset } = projectManifest(project);
|
|
951
979
|
if (preset) try {
|
|
@@ -960,6 +988,12 @@ function projectStack(project) {
|
|
|
960
988
|
storage: project.profiles.includes("s3") ? "s3" : "local"
|
|
961
989
|
};
|
|
962
990
|
}
|
|
991
|
+
/** Notifications are on when the backend's INSTALLED_APPS has Content Studio's notifications app. */
|
|
992
|
+
function notificationsStack(project) {
|
|
993
|
+
const settings = join(project.directory, "backend", "config", "settings.py");
|
|
994
|
+
if (!existsSync(settings)) return {};
|
|
995
|
+
return { notifications: /^ *["']content_studio\.notifications["'],/m.test(readFileSync(settings, "utf8")) ? "on" : "off" };
|
|
996
|
+
}
|
|
963
997
|
const REDIS = /^(redis|rediss|valkey|valkeys):/;
|
|
964
998
|
/**
|
|
965
999
|
* The cache and the tasks a project's `.env` runs. Projects from before 0.7
|
|
@@ -1155,6 +1189,25 @@ function cacheEnv(stack) {
|
|
|
1155
1189
|
...TASK_ENV[taskQueue(stack)]
|
|
1156
1190
|
};
|
|
1157
1191
|
}
|
|
1192
|
+
/** The line of Content Studio's notifications app in the backend's INSTALLED_APPS. */
|
|
1193
|
+
const NOTIFICATIONS_APP = " \"content_studio.notifications\", # notifications in the admin and by email\n";
|
|
1194
|
+
const NOTIFICATIONS_PATTERN = /^ *["']content_studio\.notifications["'],.*\n/m;
|
|
1195
|
+
/**
|
|
1196
|
+
* Adds or removes Content Studio's notifications app in the text of
|
|
1197
|
+
* `backend/config/settings.py`. Throws when it can't find where it goes.
|
|
1198
|
+
*/
|
|
1199
|
+
function setNotificationsApp(settingsText, on) {
|
|
1200
|
+
const present = NOTIFICATIONS_PATTERN.test(settingsText);
|
|
1201
|
+
if (!on) return settingsText.replace(NOTIFICATIONS_PATTERN, "");
|
|
1202
|
+
if (present) return settingsText;
|
|
1203
|
+
const after = [
|
|
1204
|
+
/^ *["']content_studio\.contrib\.password_reset["'],.*\n/m,
|
|
1205
|
+
/^ *["']content_studio\.media_library["'],.*\n/m,
|
|
1206
|
+
/^ *["']content_studio["'],.*\n/m
|
|
1207
|
+
].find((pattern) => pattern.test(settingsText));
|
|
1208
|
+
if (!after) throw new Error("backend/config/settings.py has no \"content_studio\" in INSTALLED_APPS; add \"content_studio.notifications\" there yourself.");
|
|
1209
|
+
return settingsText.replace(after, (line) => line + NOTIFICATIONS_APP);
|
|
1210
|
+
}
|
|
1158
1211
|
/** Whether `.env` values use the Valkey service (the `valkey` profile). */
|
|
1159
1212
|
function usesValkey(env) {
|
|
1160
1213
|
return [env.CACHE_URL, env.TASKS_URL].some((url) => /^(redis|rediss|valkey|valkeys):\/\/cache[:/]/.test(url ?? ""));
|
|
@@ -1256,6 +1309,10 @@ function scaffold(options) {
|
|
|
1256
1309
|
const rendered = render(text, values);
|
|
1257
1310
|
if (rendered !== text) writeFileSync(file, rendered);
|
|
1258
1311
|
}
|
|
1312
|
+
if (options.notifications === false) {
|
|
1313
|
+
const settings = join(target, "backend", "config", "settings.py");
|
|
1314
|
+
writeFileSync(settings, setNotificationsApp(readFileSync(settings, "utf8"), false));
|
|
1315
|
+
}
|
|
1259
1316
|
if (options.timeZone && options.timeZone !== "UTC") {
|
|
1260
1317
|
const settings = join(target, "backend", "config", "settings.py");
|
|
1261
1318
|
const text = readFileSync(settings, "utf8");
|
|
@@ -1285,6 +1342,7 @@ function scaffold(options) {
|
|
|
1285
1342
|
env = setEnvValues(env, { COMPOSE_PROFILES: profiles.join(",") });
|
|
1286
1343
|
if (options.ports) env = setEnvValues(env, portValues(parseEnv(env), options.ports));
|
|
1287
1344
|
if (!options.demoContent) env = setEnvValues(env, { SEED_DEMO_CONTENT: "0" });
|
|
1345
|
+
if (options.errors === "off") env = setEnvValues(env, { ERRORS: "off" });
|
|
1288
1346
|
writeFileSync(join(target, ".env"), env, { mode: 384 });
|
|
1289
1347
|
writeFileSync(join(target, ".env.example"), envExample);
|
|
1290
1348
|
rmSync(join(target, "env.example"));
|
|
@@ -1441,4 +1499,4 @@ function productionEnv(devEnv, options) {
|
|
|
1441
1499
|
todo
|
|
1442
1500
|
};
|
|
1443
1501
|
}
|
|
1444
|
-
export {
|
|
1502
|
+
export { dockerRunning as $, projectStack as A, decodePreset as B, RUN_DOMAIN as C, composeOutput as D, compose as E, DEFAULT_STACK as F, DEV_PORTS as G, pendingChoices as H, NAMED_PRESETS as I, envPorts as J, chooseFreePorts as K, PROMPT_ORDER as L, readLocalRun as M, readProject as N, findProjectDirectory as O, runningServices as P, dockerNotRunning as Q, STACK_OPTIONS as R, generateSecret as S, activeProject as T, resolvePreset as U, encodePreset as V, stackOption as W, runDoctor as X, portValues as Y, checkTools as Z, validateProjectName as _, LOCAL_S3 as a, PACKAGE_NAME as b, lockBackend as c, scaffold as d, isDockerDown as et, setNotificationsApp as f, usesValkey as g, umamiEnv as h, FRONTENDS as i, projectUrls as j, parseProfiles as k, normalizeDatabaseUrl as l, toProjectName as m, productionEnv as n, quoteEnvValue as nt, cacheEnv as o, timeZones as p, describeMove as q, validateDomain as r, setEnvValues as rt, localS3 as s, missingS3 as t, parseEnv as tt, relativeDirectory as u, validateTimeZone as v, RUN_ENV as w, generateProjectSecrets as x, CLI_VERSION as y, applies as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@django-stack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The production-ready Django, batteries included stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Leon van der Grient",
|
|
@@ -20,16 +20,15 @@ WORKDIR /app
|
|
|
20
20
|
FROM base AS deps
|
|
21
21
|
COPY pyproject.toml uv.lock* ./
|
|
22
22
|
COPY vendor/ vendor/
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
24
|
+
RUN uv sync --no-dev --no-install-project --no-cache
|
|
25
25
|
|
|
26
26
|
# Development runs as root, so files the container writes into the mounted
|
|
27
27
|
# source (such as new migrations) work on every host. Production does not.
|
|
28
28
|
# The entrypoint runs with sh, so it works without the executable bit (the
|
|
29
29
|
# mounted source keeps the host's file modes).
|
|
30
30
|
FROM deps AS dev
|
|
31
|
-
RUN --
|
|
32
|
-
uv sync --no-install-project
|
|
31
|
+
RUN uv sync --no-install-project --no-cache
|
|
33
32
|
ENTRYPOINT ["sh", "/app/entrypoint.sh"]
|
|
34
33
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
|
35
34
|
|
|
@@ -44,4 +43,4 @@ EXPOSE 8000
|
|
|
44
43
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
|
|
45
44
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/readyz', timeout=4)"
|
|
46
45
|
ENTRYPOINT ["sh", "/app/entrypoint.sh"]
|
|
47
|
-
CMD ["gunicorn", "config.asgi:application", "--worker-class", "uvicorn_worker.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "3", "--
|
|
46
|
+
CMD ["gunicorn", "config.asgi:application", "--worker-class", "uvicorn_worker.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "3", "--no-control-socket"]
|
|
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
|
|
38
38
|
"content_studio",
|
|
39
39
|
"content_studio.media_library",
|
|
40
40
|
"content_studio.contrib.password_reset",
|
|
41
|
+
"content_studio.notifications", # notifications in the admin and by email
|
|
41
42
|
"django_stack",
|
|
42
43
|
"content",
|
|
43
44
|
# Last: builds the API routes once every model is exposed.
|
|
@@ -45,6 +46,7 @@ INSTALLED_APPS = [
|
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
MIDDLEWARE = [
|
|
49
|
+
"django_stack.logs.RequestIdMiddleware", # request IDs and the access log
|
|
48
50
|
"django.middleware.security.SecurityMiddleware",
|
|
49
51
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
50
52
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
@@ -10,12 +10,16 @@ from content_studio.contrib.huey import HueyAdapter
|
|
|
10
10
|
from content_studio.dashboard.tasks import TaskDashboard
|
|
11
11
|
from content_studio.extensions import UserMenuLink
|
|
12
12
|
from django_stack.analytics import UmamiProvider
|
|
13
|
+
from django_stack.studio import SystemLink, SystemPage
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class SiteStudio(ContentStudioSite):
|
|
16
17
|
extensions = [
|
|
17
18
|
TaskDashboard(adapter=HueyAdapter()),
|
|
18
19
|
UserMenuLink(url="/tasks/", label="Tasks", icon="ph-bold ph-queue"),
|
|
20
|
+
# Superusers only: the health checks and the latest error reports.
|
|
21
|
+
SystemPage(),
|
|
22
|
+
SystemLink(),
|
|
19
23
|
]
|
|
20
24
|
|
|
21
25
|
# Page analytics from the bundled Umami when UMAMI_URL is set (see
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
from django.apps import AppConfig
|
|
1
|
+
from django.apps import AppConfig, apps
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class ContentConfig(AppConfig):
|
|
5
5
|
name = "content"
|
|
6
6
|
verbose_name = "Content"
|
|
7
|
+
|
|
8
|
+
def ready(self):
|
|
9
|
+
if apps.is_installed("content_studio.notifications"):
|
|
10
|
+
from . import notifications # noqa: F401 - registers the events
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The notifications of the site, with Content Studio's notifications app
|
|
3
|
+
(`content_studio.notifications` in INSTALLED_APPS). Editors get them in
|
|
4
|
+
the admin and, when they leave them unread, in an email digest. Each
|
|
5
|
+
editor picks which events they get on their settings page.
|
|
6
|
+
|
|
7
|
+
Register your own events here, and send them with notify(); see
|
|
8
|
+
https://djangocontentstudio.org for the API.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from content_studio.notifications import notify, register_event
|
|
12
|
+
from django.db.models.signals import post_save
|
|
13
|
+
from django.dispatch import receiver
|
|
14
|
+
from django.utils.translation import gettext_lazy as _
|
|
15
|
+
|
|
16
|
+
from .models import FormSubmission
|
|
17
|
+
|
|
18
|
+
FORM_SUBMITTED = register_event(
|
|
19
|
+
"site.form_submitted",
|
|
20
|
+
_("Contact form messages"),
|
|
21
|
+
_("A visitor sent a message with the contact form."),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@receiver(post_save, sender=FormSubmission, dispatch_uid="content_form_submitted")
|
|
26
|
+
def notify_form_submitted(sender, instance, created, raw=False, **kwargs):
|
|
27
|
+
"""Every editor who may read the messages hears about a new one."""
|
|
28
|
+
if created and not raw:
|
|
29
|
+
notify(
|
|
30
|
+
FORM_SUBMITTED.key,
|
|
31
|
+
title=_("Message from %(name)s"),
|
|
32
|
+
# The visitor's text goes in params: a "%" in it stays text.
|
|
33
|
+
body=_("%(message)s"),
|
|
34
|
+
params={"name": instance.name, "message": instance.message[:200]},
|
|
35
|
+
target=instance,
|
|
36
|
+
)
|
|
@@ -5,6 +5,7 @@ Run them with `pnpm test` at the project root.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
|
+
from django.apps import apps
|
|
8
9
|
from django.conf import settings
|
|
9
10
|
from rest_framework.test import APIClient
|
|
10
11
|
|
|
@@ -69,5 +70,22 @@ def test_contact_form(seeded):
|
|
|
69
70
|
assert FormSubmission.objects.filter(email="ada@example.com").exists()
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
@pytest.mark.skipif(not apps.is_installed("content_studio.notifications"), reason="notifications are off")
|
|
74
|
+
def test_contact_form_notifies_the_editors(seeded, django_capture_on_commit_callbacks):
|
|
75
|
+
from content_studio.notifications.models import Notification
|
|
76
|
+
from django.contrib.auth import get_user_model
|
|
77
|
+
|
|
78
|
+
editor = get_user_model().objects.create_superuser("editor", "editor@example.com", "x")
|
|
79
|
+
with django_capture_on_commit_callbacks(execute=True):
|
|
80
|
+
APIClient().post(
|
|
81
|
+
"/api/content.formsubmission",
|
|
82
|
+
{"name": "Ada", "email": "ada@example.com", "message": "100% sure"},
|
|
83
|
+
format="json",
|
|
84
|
+
)
|
|
85
|
+
notification = Notification.objects.get(recipient=editor, event="site.form_submitted")
|
|
86
|
+
assert notification.get_title() == "Message from Ada"
|
|
87
|
+
assert notification.get_body() == "100% sure"
|
|
88
|
+
|
|
89
|
+
|
|
72
90
|
def test_read_key_cannot_read_submissions(reader):
|
|
73
91
|
assert reader.get("/api/content.formsubmission").status_code == 403
|
|
@@ -5,7 +5,7 @@ description = "The Django backend of {{project_name}}."
|
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"Django>=6.1,<6.2",
|
|
8
|
-
"django-stack[server,s3]>=0.
|
|
8
|
+
"django-stack[server,s3]>=0.9.0",
|
|
9
9
|
"django-headless[schema]>=1.1.1",
|
|
10
10
|
"django-content-studio[images,huey]>=1.11.0",
|
|
11
11
|
]
|
package/templates/base/Caddyfile
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
header ?X-Frame-Options "DENY"
|
|
28
28
|
header -Server
|
|
29
29
|
|
|
30
|
+
# A request ID for every request: Django and the frontend add it to
|
|
31
|
+
# their log lines and return it in X-Request-ID, and Caddy's own log
|
|
32
|
+
# line carries it as request_id. One sent by the client is replaced.
|
|
33
|
+
request_header X-Request-ID {http.request.uuid}
|
|
34
|
+
log_append request_id {http.request.uuid}
|
|
35
|
+
|
|
30
36
|
# Uploads, when media is stored on the server instead of S3. No scripts
|
|
31
37
|
# run from uploaded files, whatever their type.
|
|
32
38
|
handle_path /media/* {
|
|
@@ -59,6 +65,6 @@
|
|
|
59
65
|
|
|
60
66
|
log {
|
|
61
67
|
output stdout
|
|
62
|
-
format
|
|
68
|
+
format json
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -26,6 +26,14 @@ x-hardened: &hardened
|
|
|
26
26
|
cap_drop:
|
|
27
27
|
- ALL
|
|
28
28
|
|
|
29
|
+
# Every service: Docker keeps at most 5 log files of 10 MB, so logs never
|
|
30
|
+
# fill the disk.
|
|
31
|
+
x-logging: &logging
|
|
32
|
+
driver: json-file
|
|
33
|
+
options:
|
|
34
|
+
max-size: 10m
|
|
35
|
+
max-file: "5"
|
|
36
|
+
|
|
29
37
|
x-frontend-env: &frontend-env
|
|
30
38
|
SITE_URL: ${SITE_URL}
|
|
31
39
|
API_URL: http://backend:8000/api
|
|
@@ -60,6 +68,14 @@ x-backend-health: &backend-health
|
|
|
60
68
|
start_period: 60s
|
|
61
69
|
retries: 30
|
|
62
70
|
|
|
71
|
+
# The worker stamps a heartbeat every minute; unhealthy when it stopped.
|
|
72
|
+
x-worker-health: &worker-health
|
|
73
|
+
test: ["CMD", "python", "manage.py", "worker_alive"]
|
|
74
|
+
interval: 30s
|
|
75
|
+
timeout: 20s
|
|
76
|
+
start_period: 120s
|
|
77
|
+
retries: 3
|
|
78
|
+
|
|
63
79
|
services:
|
|
64
80
|
# Development ------------------------------------------------------------
|
|
65
81
|
|
|
@@ -78,6 +94,7 @@ services:
|
|
|
78
94
|
- "127.0.0.1:${BACKEND_PORT:-8000}:8000"
|
|
79
95
|
depends_on: *backend-depends
|
|
80
96
|
healthcheck: *backend-health
|
|
97
|
+
logging: *logging
|
|
81
98
|
restart: unless-stopped
|
|
82
99
|
|
|
83
100
|
worker:
|
|
@@ -96,6 +113,8 @@ services:
|
|
|
96
113
|
depends_on:
|
|
97
114
|
backend:
|
|
98
115
|
condition: service_healthy
|
|
116
|
+
healthcheck: *worker-health
|
|
117
|
+
logging: *logging
|
|
99
118
|
restart: unless-stopped
|
|
100
119
|
|
|
101
120
|
frontend:
|
|
@@ -112,6 +131,7 @@ services:
|
|
|
112
131
|
depends_on:
|
|
113
132
|
backend:
|
|
114
133
|
condition: service_healthy
|
|
134
|
+
logging: *logging
|
|
115
135
|
restart: unless-stopped
|
|
116
136
|
|
|
117
137
|
mailpit:
|
|
@@ -119,6 +139,7 @@ services:
|
|
|
119
139
|
image: axllent/mailpit:v1.27
|
|
120
140
|
ports:
|
|
121
141
|
- "127.0.0.1:${MAIL_PORT:-8025}:8025"
|
|
142
|
+
logging: *logging
|
|
122
143
|
restart: unless-stopped
|
|
123
144
|
|
|
124
145
|
# A local S3 service, so development runs the same storage code as
|
|
@@ -132,6 +153,7 @@ services:
|
|
|
132
153
|
- s3data:/data
|
|
133
154
|
ports:
|
|
134
155
|
- "127.0.0.1:${S3_PORT:-8333}:8333"
|
|
156
|
+
logging: *logging
|
|
135
157
|
restart: unless-stopped
|
|
136
158
|
|
|
137
159
|
# Production -------------------------------------------------------------
|
|
@@ -160,6 +182,7 @@ services:
|
|
|
160
182
|
resources:
|
|
161
183
|
limits:
|
|
162
184
|
memory: 768M
|
|
185
|
+
logging: *logging
|
|
163
186
|
restart: unless-stopped
|
|
164
187
|
|
|
165
188
|
worker-prod:
|
|
@@ -185,13 +208,13 @@ services:
|
|
|
185
208
|
depends_on:
|
|
186
209
|
backend-prod:
|
|
187
210
|
condition: service_healthy
|
|
188
|
-
healthcheck:
|
|
189
|
-
disable: true
|
|
211
|
+
healthcheck: *worker-health
|
|
190
212
|
deploy:
|
|
191
213
|
replicas: ${TASK_WORKERS:-1}
|
|
192
214
|
resources:
|
|
193
215
|
limits:
|
|
194
216
|
memory: 512M
|
|
217
|
+
logging: *logging
|
|
195
218
|
restart: unless-stopped
|
|
196
219
|
|
|
197
220
|
frontend-prod:
|
|
@@ -212,6 +235,7 @@ services:
|
|
|
212
235
|
resources:
|
|
213
236
|
limits:
|
|
214
237
|
memory: 512M
|
|
238
|
+
logging: *logging
|
|
215
239
|
restart: unless-stopped
|
|
216
240
|
|
|
217
241
|
# The only service with public ports: HTTPS for SITE_URL with a
|
|
@@ -244,6 +268,7 @@ services:
|
|
|
244
268
|
resources:
|
|
245
269
|
limits:
|
|
246
270
|
memory: 256M
|
|
271
|
+
logging: *logging
|
|
247
272
|
restart: unless-stopped
|
|
248
273
|
|
|
249
274
|
# Shared -----------------------------------------------------------------
|
|
@@ -276,6 +301,7 @@ services:
|
|
|
276
301
|
resources:
|
|
277
302
|
limits:
|
|
278
303
|
memory: 512M
|
|
304
|
+
logging: *logging
|
|
279
305
|
restart: unless-stopped
|
|
280
306
|
|
|
281
307
|
db:
|
|
@@ -298,6 +324,7 @@ services:
|
|
|
298
324
|
resources:
|
|
299
325
|
limits:
|
|
300
326
|
memory: 1G
|
|
327
|
+
logging: *logging
|
|
301
328
|
restart: unless-stopped
|
|
302
329
|
|
|
303
330
|
# Valkey (the valkey profile), for CACHE_URL or TASKS_URL redis://cache.
|
|
@@ -318,6 +345,7 @@ services:
|
|
|
318
345
|
resources:
|
|
319
346
|
limits:
|
|
320
347
|
memory: 256M
|
|
348
|
+
logging: *logging
|
|
321
349
|
restart: unless-stopped
|
|
322
350
|
|
|
323
351
|
volumes:
|
|
@@ -114,3 +114,20 @@ BACKUP_TIME=03:00
|
|
|
114
114
|
BACKUP_KEEP=14
|
|
115
115
|
BACKUP_MEDIA=1
|
|
116
116
|
BACKUP_S3_BUCKET=
|
|
117
|
+
# Requested after each daily backup: the URL of a Healthchecks.io check or
|
|
118
|
+
# an Uptime Kuma push monitor, which alerts you when the pings stop.
|
|
119
|
+
BACKUP_PING_URL=
|
|
120
|
+
|
|
121
|
+
# Error reports (with DJANGO_DEBUG=0): each new error of the backend, the
|
|
122
|
+
# worker or a backup is mailed to ERROR_EMAIL (comma separated) or, when it
|
|
123
|
+
# is empty, to the superusers, and shows in the admin's notifications. The
|
|
124
|
+
# same error is mailed once an hour at most. ERRORS=off turns it off.
|
|
125
|
+
ERRORS=email
|
|
126
|
+
ERROR_EMAIL=
|
|
127
|
+
|
|
128
|
+
# Logs: one JSON object per line in production, plain text in development
|
|
129
|
+
# (LOG_FORMAT=json or plain). LOG_ACCESS=0 turns off Django's access log.
|
|
130
|
+
# See the logs page of the DjangoStack docs.
|
|
131
|
+
# LOG_FORMAT=json
|
|
132
|
+
# LOG_LEVEL=INFO
|
|
133
|
+
# LOG_ACCESS=1
|
|
@@ -9,12 +9,15 @@ ENV ASTRO_TELEMETRY_DISABLED=1
|
|
|
9
9
|
FROM base AS deps
|
|
10
10
|
COPY package.json package-lock.json* ./
|
|
11
11
|
COPY vendor/ vendor/
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
13
|
+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
14
14
|
|
|
15
15
|
FROM deps AS dev
|
|
16
16
|
EXPOSE 3000
|
|
17
|
-
|
|
17
|
+
# A container runs one dev server, so a lock file in the mounted .astro/ is
|
|
18
|
+
# always stale: from a stopped container, whose process IDs the new one reuses,
|
|
19
|
+
# or from `npm run dev` on the host. Astro would refuse to start next to it.
|
|
20
|
+
CMD ["sh", "-c", "rm -f .astro/dev.json && exec npm run dev"]
|
|
18
21
|
|
|
19
22
|
FROM deps AS build
|
|
20
23
|
COPY . .
|
|
@@ -9,8 +9,8 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
|
|
9
9
|
FROM base AS deps
|
|
10
10
|
COPY package.json package-lock.json* ./
|
|
11
11
|
COPY vendor/ vendor/
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
13
|
+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
14
14
|
|
|
15
15
|
FROM deps AS dev
|
|
16
16
|
EXPOSE 3000
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@django-headless/client": "^1.1.1",
|
|
16
|
-
"@django-stack/next": "^0.
|
|
17
|
-
"@django-stack/react": "^0.
|
|
18
|
-
"@django-stack/utils": "^0.
|
|
16
|
+
"@django-stack/next": "^0.9.0",
|
|
17
|
+
"@django-stack/react": "^0.9.0",
|
|
18
|
+
"@django-stack/utils": "^0.9.0",
|
|
19
19
|
"next": "^16.4.4",
|
|
20
20
|
"react": "^19.3.1",
|
|
21
21
|
"react-dom": "^19.3.1",
|
|
@@ -9,8 +9,8 @@ ENV NUXT_TELEMETRY_DISABLED=1
|
|
|
9
9
|
FROM base AS deps
|
|
10
10
|
COPY package.json package-lock.json* ./
|
|
11
11
|
COPY vendor/ vendor/
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
13
|
+
RUN if [ -f package-lock.json ]; then npm ci --ignore-scripts; else npm install --ignore-scripts; fi
|
|
14
14
|
|
|
15
15
|
FROM deps AS dev
|
|
16
16
|
EXPOSE 3000
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@django-headless/client": "^1.1.1",
|
|
18
|
-
"@django-stack/utils": "^0.
|
|
19
|
-
"@django-stack/vue": "^0.
|
|
18
|
+
"@django-stack/utils": "^0.9.0",
|
|
19
|
+
"@django-stack/vue": "^0.9.0",
|
|
20
20
|
"nuxt": "^4.6.3",
|
|
21
21
|
"nuxt-security": "^2.7.0",
|
|
22
22
|
"vue": "^3.5.44",
|
|
@@ -8,8 +8,8 @@ WORKDIR /app
|
|
|
8
8
|
FROM base AS deps
|
|
9
9
|
COPY package.json package-lock.json* ./
|
|
10
10
|
COPY vendor/ vendor/
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
12
|
+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
13
13
|
|
|
14
14
|
FROM deps AS dev
|
|
15
15
|
EXPOSE 3000
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@django-headless/client": "^1.1.1",
|
|
22
|
-
"@django-stack/svelte": "^0.
|
|
23
|
-
"@django-stack/utils": "^0.
|
|
22
|
+
"@django-stack/svelte": "^0.9.0",
|
|
23
|
+
"@django-stack/utils": "^0.9.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@sveltejs/adapter-node": "^6.0.1",
|