@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smi-digital/create-smi-app",
3
- "version": "2.7.6",
3
+ "version": "2.9.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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
- # Uploads stay on local disk for now; DB lives in Postgres (no sqlite volume).
11
- - ./data/uploads:/opt/app/public/uploads
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
- - "traefik.enable=true"
22
- # Route traffic from your backend domain to this container
23
- - "traefik.http.routers.__APP_NAME__-strapi.rule=Host(`__BACKEND_DOMAIN__`)"
24
- # Request an SSL certificate from the global Let's Encrypt resolver
25
- - "traefik.http.routers.__APP_NAME__-strapi.tls.certresolver=letsencrypt"
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
- - ./frontend/nginx-cache.conf:/etc/nginx/conf.d/default.conf:ro
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
- - __APP_NAME__-astro
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): the read-only config mount blocks nginx's
68
- # IPv6 listen, but busybox wget resolves localhost to ::1 first → refused.
75
+ # Use 127.0.0.1 (not localhost): busybox wget resolves localhost to ::1
76
+ # first, and the frontend block only listens on IPv4connection 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 www and non-www
79
- - "traefik.http.routers.__APP_NAME__-astro.rule=Host(`__PROJECT_DOMAIN__`) || Host(`www.__PROJECT_DOMAIN__`)"
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
- # Route traffic to the Nginx Cache (Port 80) instead of Astro directly
84
- - "traefik.http.services.__APP_NAME__-astro.loadbalancer.server.port=80"
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
- proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=astro_cache:10m max_size=1g inactive=60m use_temp_path=off;
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
- server {
4
- listen 80;
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
- # Gzip compression for faster transfers
7
- gzip on;
8
- gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
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. Returns 200 from nginx
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
- # Respect the Stale-While-Revalidate headers from Astro middleware
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
- # Add a header so we can verify if the cache is working
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
  }