@smi-digital/create-smi-app 2.7.6 → 2.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/package.json +1 -1
- package/templates/integrations/strapi-astro/.github/workflows/deploy.yml.template +3 -0
- package/templates/integrations/strapi-astro/Dockerfile.backend.template +10 -0
- package/templates/integrations/strapi-astro/Dockerfile.frontend.ssr.template +11 -0
- package/templates/integrations/strapi-astro/docker-compose.yml.template +41 -23
- package/templates/integrations/strapi-astro/docker-entrypoint.sh.template +22 -0
- package/templates/integrations/strapi-astro/integration.config.json +4 -6
- package/templates/integrations/strapi-astro/nginx-cache.conf.template +75 -12
package/package.json
CHANGED
|
@@ -13,6 +13,9 @@ jobs:
|
|
|
13
13
|
project_domain: __PROJECT_DOMAIN__
|
|
14
14
|
backend_domain: __BACKEND_DOMAIN__
|
|
15
15
|
app_name: __APP_NAME__
|
|
16
|
+
# Optional: 301-redirect extra domains to project_domain (space-separated).
|
|
17
|
+
# Each must have DNS pointing at the server so Traefik can issue its cert.
|
|
18
|
+
# redirect_domains: "old-domain.com www.old-domain.com"
|
|
16
19
|
secrets:
|
|
17
20
|
# Per-repo secrets (Settings → Secrets and variables → Actions).
|
|
18
21
|
# WEBSERVER_SSH_KEY and ZOT_PASSWORD are shared across all projects;
|
|
@@ -33,5 +33,15 @@ ENV PATH=/opt/node_modules/.bin:$PATH
|
|
|
33
33
|
WORKDIR /opt/app
|
|
34
34
|
COPY --from=builder /opt/app ./
|
|
35
35
|
|
|
36
|
+
# Keep a pristine copy of the image's static public files (robots.txt, etc.).
|
|
37
|
+
# public/ is a mounted volume at runtime, so the entrypoint reseeds from here.
|
|
38
|
+
RUN cp -r /opt/app/public /opt/app/public-defaults
|
|
39
|
+
|
|
40
|
+
# Entrypoint seeds static public files into the mounted volume on startup,
|
|
41
|
+
# leaving uploaded media untouched. See docker-entrypoint.sh for why.
|
|
42
|
+
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
|
43
|
+
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
44
|
+
|
|
36
45
|
EXPOSE 1337
|
|
46
|
+
ENTRYPOINT ["docker-entrypoint.sh"]
|
|
37
47
|
CMD ["npm", "run", "start"]
|
|
@@ -12,6 +12,17 @@ WORKDIR /app
|
|
|
12
12
|
COPY package.json package-lock.json ./
|
|
13
13
|
RUN npm ci
|
|
14
14
|
COPY . .
|
|
15
|
+
|
|
16
|
+
# PUBLIC_* are inlined into the CLIENT bundle at build time (they reach the
|
|
17
|
+
# browser). The deploy passes these as --build-arg from project_domain/
|
|
18
|
+
# backend_domain, so the image targets the right site/CMS per environment.
|
|
19
|
+
ARG PUBLIC_SITE_URL=
|
|
20
|
+
ARG PUBLIC_STRAPI_URL=
|
|
21
|
+
ARG PUBLIC_GA_ID=
|
|
22
|
+
ENV PUBLIC_SITE_URL=${PUBLIC_SITE_URL}
|
|
23
|
+
ENV PUBLIC_STRAPI_URL=${PUBLIC_STRAPI_URL}
|
|
24
|
+
ENV PUBLIC_GA_ID=${PUBLIC_GA_ID}
|
|
25
|
+
|
|
15
26
|
RUN npm run build
|
|
16
27
|
|
|
17
28
|
# Stage 2: Serve the SSR server bundle
|
|
@@ -7,8 +7,11 @@ services:
|
|
|
7
7
|
env_file:
|
|
8
8
|
- ./backend.env
|
|
9
9
|
volumes:
|
|
10
|
-
#
|
|
11
|
-
|
|
10
|
+
# Mount the whole public/ dir (not just public/uploads) so `strapi transfer`
|
|
11
|
+
# can move public/uploads during restore — a bind-mounted uploads SUBdir is a
|
|
12
|
+
# mount point and cannot be moved, which breaks transfers. DB lives in Postgres
|
|
13
|
+
# (no sqlite volume). The entrypoint reseeds static files (robots.txt) here.
|
|
14
|
+
- ./data/public:/opt/app/public
|
|
12
15
|
networks:
|
|
13
16
|
- agency_shared_network
|
|
14
17
|
healthcheck:
|
|
@@ -18,19 +21,11 @@ services:
|
|
|
18
21
|
timeout: 5s
|
|
19
22
|
retries: 15
|
|
20
23
|
labels:
|
|
21
|
-
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
- "traefik.
|
|
26
|
-
# Tell Traefik which port to send traffic to inside the container
|
|
27
|
-
- "traefik.http.services.__APP_NAME__-strapi.loadbalancer.server.port=1337"
|
|
28
|
-
# Strapi requires larger body sizes for media uploads (25MB)
|
|
29
|
-
- "traefik.http.middlewares.__APP_NAME__-strapi-limit.buffering.maxRequestBodyBytes=25000000"
|
|
30
|
-
# Global middlewares (defined in webhosting-infra/traefik/dynamic) + the local
|
|
31
|
-
# body-size limit. rate-limit first so abusive traffic is dropped early; this
|
|
32
|
-
# matters most on the public Strapi admin/API.
|
|
33
|
-
- "traefik.http.routers.__APP_NAME__-strapi.middlewares=rate-limit@file,secure-headers@file,__APP_NAME__-strapi-limit"
|
|
24
|
+
# Strapi is no longer exposed to Traefik directly: the central
|
|
25
|
+
# __APP_NAME__-cache nginx now fronts BACKEND_DOMAIN and proxies here
|
|
26
|
+
# (caching /uploads). Keeping it internal means all backend traffic —
|
|
27
|
+
# admin, API and media — flows through the shared cache.
|
|
28
|
+
- "traefik.enable=false"
|
|
34
29
|
|
|
35
30
|
__APP_NAME__-astro:
|
|
36
31
|
container_name: __APP_NAME__-astro
|
|
@@ -57,15 +52,28 @@ services:
|
|
|
57
52
|
container_name: __APP_NAME__-cache
|
|
58
53
|
image: nginx:alpine
|
|
59
54
|
restart: unless-stopped
|
|
55
|
+
environment:
|
|
56
|
+
# Injected into nginx-cache.conf via the nginx image's envsubst at start
|
|
57
|
+
# so this one proxy can server-name-route both public hosts. The filter
|
|
58
|
+
# restricts substitution to these vars, leaving nginx's own runtime
|
|
59
|
+
# $variables ($host, $upstream_cache_status, …) untouched.
|
|
60
|
+
PROJECT_DOMAIN: ${PROJECT_DOMAIN}
|
|
61
|
+
BACKEND_DOMAIN: ${BACKEND_DOMAIN}
|
|
62
|
+
NGINX_ENVSUBST_FILTER: "^(PROJECT_DOMAIN|BACKEND_DOMAIN)$$"
|
|
60
63
|
volumes:
|
|
61
|
-
|
|
64
|
+
# Mounted as a template: the nginx entrypoint runs envsubst and writes the
|
|
65
|
+
# rendered config to conf.d/ before starting.
|
|
66
|
+
- ./frontend/nginx-cache.conf:/etc/nginx/templates/default.conf.template:ro
|
|
62
67
|
networks:
|
|
63
68
|
- agency_shared_network
|
|
64
69
|
depends_on:
|
|
65
|
-
|
|
70
|
+
__APP_NAME__-astro:
|
|
71
|
+
condition: service_healthy
|
|
72
|
+
__APP_NAME__-strapi:
|
|
73
|
+
condition: service_healthy
|
|
66
74
|
healthcheck:
|
|
67
|
-
# Use 127.0.0.1 (not localhost):
|
|
68
|
-
#
|
|
75
|
+
# Use 127.0.0.1 (not localhost): busybox wget resolves localhost to ::1
|
|
76
|
+
# first, and the frontend block only listens on IPv4 → connection refused.
|
|
69
77
|
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/healthz"]
|
|
70
78
|
interval: 5s
|
|
71
79
|
timeout: 2s
|
|
@@ -75,13 +83,23 @@ services:
|
|
|
75
83
|
order: start-first
|
|
76
84
|
labels:
|
|
77
85
|
- "traefik.enable=true"
|
|
78
|
-
# Listen on both
|
|
79
|
-
|
|
86
|
+
# Frontend host(s) → this cache (Astro SSR behind it). Listen on both
|
|
87
|
+
# www and non-www.
|
|
88
|
+
- "traefik.http.routers.__APP_NAME__-astro.rule=Host(`${PROJECT_DOMAIN}`) || Host(`www.${PROJECT_DOMAIN}`)"
|
|
80
89
|
- "traefik.http.routers.__APP_NAME__-astro.tls.certresolver=letsencrypt"
|
|
81
90
|
# Global middlewares (defined in webhosting-infra/traefik/dynamic)
|
|
82
91
|
- "traefik.http.routers.__APP_NAME__-astro.middlewares=secure-headers@file,rate-limit@file"
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
- "traefik.http.routers.__APP_NAME__-astro.service=__APP_NAME__-cache"
|
|
93
|
+
# Backend host → this same cache (Strapi behind it; /uploads cached). The
|
|
94
|
+
# 25 MB upload body limit + rate-limit/secure-headers apply on this route.
|
|
95
|
+
- "traefik.http.routers.__APP_NAME__-strapi.rule=Host(`${BACKEND_DOMAIN}`)"
|
|
96
|
+
- "traefik.http.routers.__APP_NAME__-strapi.tls.certresolver=letsencrypt"
|
|
97
|
+
- "traefik.http.middlewares.__APP_NAME__-strapi-limit.buffering.maxRequestBodyBytes=25000000"
|
|
98
|
+
- "traefik.http.routers.__APP_NAME__-strapi.middlewares=rate-limit@file,secure-headers@file,__APP_NAME__-strapi-limit"
|
|
99
|
+
- "traefik.http.routers.__APP_NAME__-strapi.service=__APP_NAME__-cache"
|
|
100
|
+
# Both routers share this container's single service (Port 80), which
|
|
101
|
+
# proxies to Astro / Strapi by server_name.
|
|
102
|
+
- "traefik.http.services.__APP_NAME__-cache.loadbalancer.server.port=80"
|
|
85
103
|
|
|
86
104
|
networks:
|
|
87
105
|
agency_shared_network:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# The whole public/ directory is a mounted volume. We mount the parent (not just
|
|
5
|
+
# public/uploads) so that `strapi transfer` can move public/uploads -> a backup
|
|
6
|
+
# folder during restore: a bind-mounted uploads SUBdir is a mount point and the
|
|
7
|
+
# kernel refuses to rename a mount point, which breaks transfers. With the parent
|
|
8
|
+
# mounted, uploads is a normal subdir and behaves exactly like a non-Docker Strapi.
|
|
9
|
+
#
|
|
10
|
+
# Because the volume shadows the image's baked public/ (robots.txt, etc.), reseed
|
|
11
|
+
# those static files on startup — but never touch uploaded media (public/uploads).
|
|
12
|
+
mkdir -p /opt/app/public/uploads
|
|
13
|
+
|
|
14
|
+
if [ -d /opt/app/public-defaults ]; then
|
|
15
|
+
for item in /opt/app/public-defaults/* /opt/app/public-defaults/.[!.]*; do
|
|
16
|
+
[ -e "$item" ] || continue
|
|
17
|
+
[ "$(basename "$item")" = "uploads" ] && continue
|
|
18
|
+
cp -rf "$item" /opt/app/public/
|
|
19
|
+
done
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exec "$@"
|
|
@@ -29,12 +29,6 @@
|
|
|
29
29
|
"default": "my-new-client",
|
|
30
30
|
"required": true,
|
|
31
31
|
"validate": "slug"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"key": "include_www",
|
|
35
|
-
"message": "Include www domain?",
|
|
36
|
-
"type": "confirm",
|
|
37
|
-
"default": false
|
|
38
32
|
}
|
|
39
33
|
]
|
|
40
34
|
},
|
|
@@ -63,6 +57,10 @@
|
|
|
63
57
|
"template": "Dockerfile.backend.template",
|
|
64
58
|
"target": "backend/Dockerfile"
|
|
65
59
|
},
|
|
60
|
+
{
|
|
61
|
+
"template": "docker-entrypoint.sh.template",
|
|
62
|
+
"target": "backend/docker-entrypoint.sh"
|
|
63
|
+
},
|
|
66
64
|
{
|
|
67
65
|
"template": "Dockerfile.frontend.ssr.template",
|
|
68
66
|
"target": "frontend/Dockerfile"
|
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# Central reverse-proxy + cache for the whole project. ONE nginx fronts BOTH
|
|
3
|
+
# public hosts and dispatches by server_name:
|
|
4
|
+
# • ${PROJECT_DOMAIN} → Astro SSR (stale-while-revalidate page cache)
|
|
5
|
+
# • ${BACKEND_DOMAIN} → Strapi (admin/API uncached; /uploads cached)
|
|
6
|
+
#
|
|
7
|
+
# Domains are injected at container start by the nginx image's envsubst
|
|
8
|
+
# (it renders /etc/nginx/templates/*.template → /etc/nginx/conf.d/). The
|
|
9
|
+
# compose service restricts substitution to PROJECT_DOMAIN/BACKEND_DOMAIN via
|
|
10
|
+
# NGINX_ENVSUBST_FILTER, so nginx's own runtime variables ($host, $scheme,
|
|
11
|
+
# $upstream_cache_status, …) are left untouched.
|
|
12
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
2
13
|
|
|
3
|
-
|
|
4
|
-
|
|
14
|
+
# Separate cache zones: short-lived SWR for SSR HTML, long-lived for media.
|
|
15
|
+
proxy_cache_path /var/cache/nginx/astro levels=1:2 keys_zone=astro_cache:10m max_size=1g inactive=60m use_temp_path=off;
|
|
16
|
+
proxy_cache_path /var/cache/nginx/strapi levels=1:2 keys_zone=strapi_cache:10m max_size=2g inactive=7d use_temp_path=off;
|
|
5
17
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
gzip on;
|
|
19
|
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
|
20
|
+
|
|
21
|
+
# ── Frontend: Astro SSR ───────────────────────────────────────────────────
|
|
22
|
+
# default_server so the container healthcheck (Host: 127.0.0.1) and any
|
|
23
|
+
# unmatched host land here rather than on the Strapi block.
|
|
24
|
+
server {
|
|
25
|
+
listen 80 default_server;
|
|
26
|
+
server_name ${PROJECT_DOMAIN} www.${PROJECT_DOMAIN};
|
|
9
27
|
|
|
10
|
-
# Liveness endpoint for the container healthcheck
|
|
28
|
+
# Liveness endpoint for the container healthcheck — answered by nginx
|
|
11
29
|
# itself, so health reflects "the proxy is up" rather than the upstream
|
|
12
30
|
# homepage's status code.
|
|
13
31
|
location = /healthz {
|
|
@@ -18,21 +36,66 @@ server {
|
|
|
18
36
|
|
|
19
37
|
location / {
|
|
20
38
|
proxy_cache astro_cache;
|
|
21
|
-
|
|
22
|
-
#
|
|
39
|
+
|
|
40
|
+
# Stale-While-Revalidate: serve fast, refresh in the background.
|
|
23
41
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
24
42
|
proxy_cache_background_update on;
|
|
25
43
|
proxy_cache_lock on;
|
|
26
44
|
|
|
27
|
-
# Proxy to the Astro SSR container (internal Docker DNS)
|
|
45
|
+
# Proxy to the Astro SSR container (internal Docker DNS).
|
|
28
46
|
proxy_pass http://__APP_NAME__-astro:4321;
|
|
29
|
-
|
|
47
|
+
|
|
48
|
+
proxy_set_header Host $host;
|
|
49
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
50
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
51
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
52
|
+
|
|
53
|
+
add_header X-Cache-Status $upstream_cache_status;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ── Backend: Strapi ───────────────────────────────────────────────────────
|
|
58
|
+
server {
|
|
59
|
+
listen 80;
|
|
60
|
+
server_name ${BACKEND_DOMAIN};
|
|
61
|
+
|
|
62
|
+
# Match the Traefik buffering limit so media uploads aren't capped at
|
|
63
|
+
# nginx's 1 MB default.
|
|
64
|
+
client_max_body_size 25m;
|
|
65
|
+
|
|
66
|
+
# Uploaded media: Strapi names files with a content hash, so they're
|
|
67
|
+
# effectively immutable — cache hard at the edge and in the browser, and
|
|
68
|
+
# serve stale while refreshing.
|
|
69
|
+
location /uploads/ {
|
|
70
|
+
proxy_cache strapi_cache;
|
|
71
|
+
proxy_cache_valid 200 30d;
|
|
72
|
+
proxy_cache_use_stale error timeout updating;
|
|
73
|
+
proxy_cache_background_update on;
|
|
74
|
+
proxy_cache_lock on;
|
|
75
|
+
|
|
76
|
+
# Media is static: ignore any cookie/cache headers Strapi attaches so
|
|
77
|
+
# the response is always cacheable, and never relay a Set-Cookie.
|
|
78
|
+
proxy_ignore_headers Set-Cookie Cache-Control Expires;
|
|
79
|
+
proxy_hide_header Set-Cookie;
|
|
80
|
+
|
|
81
|
+
proxy_pass http://__APP_NAME__-strapi:1337;
|
|
82
|
+
|
|
30
83
|
proxy_set_header Host $host;
|
|
31
84
|
proxy_set_header X-Real-IP $remote_addr;
|
|
32
85
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
33
86
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
34
87
|
|
|
35
|
-
|
|
88
|
+
add_header Cache-Control "public, max-age=2592000";
|
|
36
89
|
add_header X-Cache-Status $upstream_cache_status;
|
|
37
90
|
}
|
|
91
|
+
|
|
92
|
+
# Admin panel + content API: authenticated and live — never cache.
|
|
93
|
+
location / {
|
|
94
|
+
proxy_pass http://__APP_NAME__-strapi:1337;
|
|
95
|
+
|
|
96
|
+
proxy_set_header Host $host;
|
|
97
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
98
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
99
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
100
|
+
}
|
|
38
101
|
}
|