@smi-digital/create-smi-app 2.8.0 → 2.9.1

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.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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 "$@"
@@ -57,6 +57,10 @@
57
57
  "template": "Dockerfile.backend.template",
58
58
  "target": "backend/Dockerfile"
59
59
  },
60
+ {
61
+ "template": "docker-entrypoint.sh.template",
62
+ "target": "backend/docker-entrypoint.sh"
63
+ },
60
64
  {
61
65
  "template": "Dockerfile.frontend.ssr.template",
62
66
  "target": "frontend/Dockerfile"
@@ -1,13 +1,39 @@
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
+ # WebSocket upgrade support `strapi transfer` and some admin features open a
19
+ # WebSocket; without this the upgrade collapses to a plain 200 and the transfer
20
+ # fails with "Unexpected server response 200".
21
+ map $http_upgrade $connection_upgrade {
22
+ default upgrade;
23
+ '' close;
24
+ }
9
25
 
10
- # Liveness endpoint for the container healthcheck. Returns 200 from nginx
26
+ gzip on;
27
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
28
+
29
+ # ── Frontend: Astro SSR ───────────────────────────────────────────────────
30
+ # default_server so the container healthcheck (Host: 127.0.0.1) and any
31
+ # unmatched host land here rather than on the Strapi block.
32
+ server {
33
+ listen 80 default_server;
34
+ server_name ${PROJECT_DOMAIN} www.${PROJECT_DOMAIN};
35
+
36
+ # Liveness endpoint for the container healthcheck — answered by nginx
11
37
  # itself, so health reflects "the proxy is up" rather than the upstream
12
38
  # homepage's status code.
13
39
  location = /healthz {
@@ -18,21 +44,74 @@ server {
18
44
 
19
45
  location / {
20
46
  proxy_cache astro_cache;
21
-
22
- # Respect the Stale-While-Revalidate headers from Astro middleware
47
+
48
+ # Stale-While-Revalidate: serve fast, refresh in the background.
23
49
  proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
24
50
  proxy_cache_background_update on;
25
51
  proxy_cache_lock on;
26
52
 
27
- # Proxy to the Astro SSR container (internal Docker DNS)
53
+ # Proxy to the Astro SSR container (internal Docker DNS).
28
54
  proxy_pass http://__APP_NAME__-astro:4321;
29
-
55
+
30
56
  proxy_set_header Host $host;
31
57
  proxy_set_header X-Real-IP $remote_addr;
32
58
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33
59
  proxy_set_header X-Forwarded-Proto $scheme;
34
60
 
35
- # Add a header so we can verify if the cache is working
36
61
  add_header X-Cache-Status $upstream_cache_status;
37
62
  }
38
63
  }
64
+
65
+ # ── Backend: Strapi ───────────────────────────────────────────────────────
66
+ server {
67
+ listen 80;
68
+ server_name ${BACKEND_DOMAIN};
69
+
70
+ # Match the Traefik buffering limit so media uploads aren't capped at
71
+ # nginx's 1 MB default.
72
+ client_max_body_size 25m;
73
+
74
+ # Uploaded media: Strapi names files with a content hash, so they're
75
+ # effectively immutable — cache hard at the edge and in the browser, and
76
+ # serve stale while refreshing.
77
+ location /uploads/ {
78
+ proxy_cache strapi_cache;
79
+ proxy_cache_valid 200 30d;
80
+ proxy_cache_use_stale error timeout updating;
81
+ proxy_cache_background_update on;
82
+ proxy_cache_lock on;
83
+
84
+ # Media is static: ignore any cookie/cache headers Strapi attaches so
85
+ # the response is always cacheable, and never relay a Set-Cookie.
86
+ proxy_ignore_headers Set-Cookie Cache-Control Expires;
87
+ proxy_hide_header Set-Cookie;
88
+
89
+ proxy_pass http://__APP_NAME__-strapi:1337;
90
+
91
+ proxy_set_header Host $host;
92
+ proxy_set_header X-Real-IP $remote_addr;
93
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
94
+ proxy_set_header X-Forwarded-Proto $scheme;
95
+
96
+ add_header Cache-Control "public, max-age=2592000";
97
+ add_header X-Cache-Status $upstream_cache_status;
98
+ }
99
+
100
+ # Admin panel + content API: authenticated and live — never cache.
101
+ location / {
102
+ proxy_pass http://__APP_NAME__-strapi:1337;
103
+
104
+ # WebSocket upgrade (strapi transfer / admin). Long timeouts so a big
105
+ # data transfer isn't dropped mid-stream.
106
+ proxy_http_version 1.1;
107
+ proxy_set_header Upgrade $http_upgrade;
108
+ proxy_set_header Connection $connection_upgrade;
109
+ proxy_read_timeout 3600s;
110
+ proxy_send_timeout 3600s;
111
+
112
+ proxy_set_header Host $host;
113
+ proxy_set_header X-Real-IP $remote_addr;
114
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
115
+ proxy_set_header X-Forwarded-Proto $scheme;
116
+ }
117
+ }