create-projx 1.6.2 → 1.6.4

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.
@@ -33,7 +33,7 @@ import {
33
33
  upsertComponentMarker,
34
34
  writeComponentMarker,
35
35
  writeProjxConfig
36
- } from "./chunk-LTIJPVRZ.js";
36
+ } from "./chunk-6YRBHJ2V.js";
37
37
  export {
38
38
  COMPONENTS,
39
39
  COMPONENT_MARKER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,7 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
30
30
  ## Getting Started
31
31
 
32
32
  ```bash
33
- ./setup.sh # Install all dependencies
33
+ ./scripts/setup.sh # Install all dependencies
34
34
  docker compose -f docker-compose.dev.yml up # Start with Docker (dev mode)
35
35
  ```
36
36
  <% if (components.includes('fastapi')) { %>
@@ -13,23 +13,23 @@ jobs:
13
13
  permissions:
14
14
  pull-requests: read
15
15
  outputs:
16
- <% if (components.includes('fastapi')) { %>
17
- <%= paths.fastapi %>: ${{ steps.filter.outputs.<%= paths.fastapi %> }}
16
+ <% for (const inst of fastapiInstances) { %>
17
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
18
18
  <% } %>
19
- <% if (components.includes('fastify')) { %>
20
- <%= paths.fastify %>: ${{ steps.filter.outputs.<%= paths.fastify %> }}
19
+ <% for (const inst of fastifyInstances) { %>
20
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
21
21
  <% } %>
22
- <% if (components.includes('frontend')) { %>
23
- <%= paths.frontend %>: ${{ steps.filter.outputs.<%= paths.frontend %> }}
22
+ <% for (const inst of frontendInstances) { %>
23
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
24
24
  <% } %>
25
- <% if (components.includes('mobile')) { %>
26
- <%= paths.mobile %>: ${{ steps.filter.outputs.<%= paths.mobile %> }}
25
+ <% for (const inst of mobileInstances) { %>
26
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
27
27
  <% } %>
28
- <% if (components.includes('e2e')) { %>
29
- <%= paths.e2e %>: ${{ steps.filter.outputs.<%= paths.e2e %> }}
28
+ <% for (const inst of e2eInstances) { %>
29
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
30
30
  <% } %>
31
- <% if (components.includes('infra')) { %>
32
- <%= paths.infra %>: ${{ steps.filter.outputs.<%= paths.infra %> }}
31
+ <% for (const inst of infraInstances) { %>
32
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
33
33
  <% } %>
34
34
  steps:
35
35
  - uses: actions/checkout@v5
@@ -37,29 +37,29 @@ jobs:
37
37
  id: filter
38
38
  with:
39
39
  filters: |
40
- <% if (components.includes('fastapi')) { %>
41
- <%= paths.fastapi %>:
42
- - '<%= paths.fastapi %>/**'
40
+ <% for (const inst of fastapiInstances) { %>
41
+ <%= inst.path %>:
42
+ - '<%= inst.path %>/**'
43
43
  <% } %>
44
- <% if (components.includes('fastify')) { %>
45
- <%= paths.fastify %>:
46
- - '<%= paths.fastify %>/**'
44
+ <% for (const inst of fastifyInstances) { %>
45
+ <%= inst.path %>:
46
+ - '<%= inst.path %>/**'
47
47
  <% } %>
48
- <% if (components.includes('frontend')) { %>
49
- <%= paths.frontend %>:
50
- - '<%= paths.frontend %>/**'
48
+ <% for (const inst of frontendInstances) { %>
49
+ <%= inst.path %>:
50
+ - '<%= inst.path %>/**'
51
51
  <% } %>
52
- <% if (components.includes('mobile')) { %>
53
- <%= paths.mobile %>:
54
- - '<%= paths.mobile %>/**'
52
+ <% for (const inst of mobileInstances) { %>
53
+ <%= inst.path %>:
54
+ - '<%= inst.path %>/**'
55
55
  <% } %>
56
- <% if (components.includes('e2e')) { %>
57
- <%= paths.e2e %>:
58
- - '<%= paths.e2e %>/**'
56
+ <% for (const inst of e2eInstances) { %>
57
+ <%= inst.path %>:
58
+ - '<%= inst.path %>/**'
59
59
  <% } %>
60
- <% if (components.includes('infra')) { %>
61
- <%= paths.infra %>:
62
- - '<%= paths.infra %>/**'
60
+ <% for (const inst of infraInstances) { %>
61
+ <%= inst.path %>:
62
+ - '<%= inst.path %>/**'
63
63
  <% } %>
64
64
 
65
65
  secrets:
@@ -72,16 +72,16 @@ jobs:
72
72
  - uses: gitleaks/gitleaks-action@v2
73
73
  env:
74
74
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75
- <% if (components.includes('fastapi')) { %>
75
+ <% for (const inst of fastapiInstances) { %>
76
76
 
77
- <%= paths.fastapi %>:
78
- name: <%= displayNames.fastapi %> (format + lint + typecheck + test + audit)
77
+ <%= inst.path %>:
78
+ name: <%= inst.display %> (format + lint + typecheck + test + audit)
79
79
  needs: changes
80
- if: needs.changes.outputs.<%= paths.fastapi %> == 'true'
80
+ if: needs.changes.outputs.<%= inst.path %> == 'true'
81
81
  runs-on: ubuntu-latest
82
82
  defaults:
83
83
  run:
84
- working-directory: <%= paths.fastapi %>
84
+ working-directory: <%= inst.path %>
85
85
  steps:
86
86
  - uses: actions/checkout@v5
87
87
  - uses: astral-sh/setup-uv@v4
@@ -92,16 +92,16 @@ jobs:
92
92
  - run: uv run pytest
93
93
  - run: uv run pip-audit
94
94
  <% } %>
95
- <% if (components.includes('fastify')) { %>
95
+ <% for (const inst of fastifyInstances) { %>
96
96
 
97
- <%= paths.fastify %>:
98
- name: <%= displayNames.fastify %> (format + lint + typecheck + audit)
97
+ <%= inst.path %>:
98
+ name: <%= inst.display %> (format + lint + typecheck + audit)
99
99
  needs: changes
100
- if: needs.changes.outputs.<%= paths.fastify %> == 'true'
100
+ if: needs.changes.outputs.<%= inst.path %> == 'true'
101
101
  runs-on: ubuntu-latest
102
102
  defaults:
103
103
  run:
104
- working-directory: <%= paths.fastify %>
104
+ working-directory: <%= inst.path %>
105
105
  steps:
106
106
  - uses: actions/checkout@v5
107
107
  <% if (pm === 'pnpm') { %>
@@ -117,7 +117,7 @@ jobs:
117
117
  with:
118
118
  node-version: 20
119
119
  cache: <%= pm.name %>
120
- cache-dependency-path: <%= paths.fastify %>/<%= pm.lockfile %>
120
+ cache-dependency-path: <%= inst.path %>/<%= pm.lockfile %>
121
121
  <% } %>
122
122
  - run: <%= pm.ci %>
123
123
  - run: <%= pm.prismaExec %> generate
@@ -126,16 +126,16 @@ jobs:
126
126
  - run: <%= pm.exec %> tsc --noEmit
127
127
  - run: <%= pm.audit %>
128
128
  <% } %>
129
- <% if (components.includes('frontend')) { %>
129
+ <% for (const inst of frontendInstances) { %>
130
130
 
131
- <%= paths.frontend %>:
132
- name: <%= displayNames.frontend %> (format + lint + typecheck + audit)
131
+ <%= inst.path %>:
132
+ name: <%= inst.display %> (format + lint + typecheck + audit)
133
133
  needs: changes
134
- if: needs.changes.outputs.<%= paths.frontend %> == 'true'
134
+ if: needs.changes.outputs.<%= inst.path %> == 'true'
135
135
  runs-on: ubuntu-latest
136
136
  defaults:
137
137
  run:
138
- working-directory: <%= paths.frontend %>
138
+ working-directory: <%= inst.path %>
139
139
  steps:
140
140
  - uses: actions/checkout@v5
141
141
  <% if (pm === 'pnpm') { %>
@@ -151,7 +151,7 @@ jobs:
151
151
  with:
152
152
  node-version: 22
153
153
  cache: <%= pm.name %>
154
- cache-dependency-path: <%= paths.frontend %>/<%= pm.lockfile %>
154
+ cache-dependency-path: <%= inst.path %>/<%= pm.lockfile %>
155
155
  <% } %>
156
156
  - run: <%= pm.ci %>
157
157
  - run: <%= pm.exec %> prettier --check .
@@ -159,16 +159,16 @@ jobs:
159
159
  - run: <%= pm.exec %> tsc --noEmit
160
160
  - run: <%= pm.audit %>
161
161
  <% } %>
162
- <% if (components.includes('mobile')) { %>
162
+ <% for (const inst of mobileInstances) { %>
163
163
 
164
- <%= paths.mobile %>:
165
- name: <%= displayNames.mobile %> (format + analyze)
164
+ <%= inst.path %>:
165
+ name: <%= inst.display %> (format + analyze)
166
166
  needs: changes
167
- if: needs.changes.outputs.<%= paths.mobile %> == 'true'
167
+ if: needs.changes.outputs.<%= inst.path %> == 'true'
168
168
  runs-on: ubuntu-latest
169
169
  defaults:
170
170
  run:
171
- working-directory: <%= paths.mobile %>
171
+ working-directory: <%= inst.path %>
172
172
  steps:
173
173
  - uses: actions/checkout@v5
174
174
  - uses: subosito/flutter-action@v2
@@ -179,16 +179,16 @@ jobs:
179
179
  - run: dart format --set-exit-if-changed .
180
180
  - run: dart analyze --fatal-infos
181
181
  <% } %>
182
- <% if (components.includes('e2e')) { %>
182
+ <% for (const inst of e2eInstances) { %>
183
183
 
184
- <%= paths.e2e %>:
185
- name: <%= displayNames.e2e %> (format + lint + typecheck + audit)
184
+ <%= inst.path %>:
185
+ name: <%= inst.display %> (format + lint + typecheck + audit)
186
186
  needs: changes
187
- if: needs.changes.outputs.<%= paths.e2e %> == 'true'
187
+ if: needs.changes.outputs.<%= inst.path %> == 'true'
188
188
  runs-on: ubuntu-latest
189
189
  defaults:
190
190
  run:
191
- working-directory: <%= paths.e2e %>
191
+ working-directory: <%= inst.path %>
192
192
  steps:
193
193
  - uses: actions/checkout@v5
194
194
  <% if (pm === 'pnpm') { %>
@@ -204,7 +204,7 @@ jobs:
204
204
  with:
205
205
  node-version: 22
206
206
  cache: <%= pm.name %>
207
- cache-dependency-path: <%= paths.e2e %>/<%= pm.lockfile %>
207
+ cache-dependency-path: <%= inst.path %>/<%= pm.lockfile %>
208
208
  <% } %>
209
209
  - run: <%= pm.ci %>
210
210
  - run: <%= pm.exec %> prettier --check .
@@ -212,16 +212,16 @@ jobs:
212
212
  - run: <%= pm.exec %> tsc --noEmit
213
213
  - run: <%= pm.audit %>
214
214
  <% } %>
215
- <% if (components.includes('infra')) { %>
215
+ <% for (const inst of infraInstances) { %>
216
216
 
217
- <%= paths.infra %>:
218
- name: <%= displayNames.infra %> (fmt + validate)
217
+ <%= inst.path %>:
218
+ name: <%= inst.display %> (fmt + validate)
219
219
  needs: changes
220
- if: needs.changes.outputs.<%= paths.infra %> == 'true'
220
+ if: needs.changes.outputs.<%= inst.path %> == 'true'
221
221
  runs-on: ubuntu-latest
222
222
  defaults:
223
223
  run:
224
- working-directory: <%= paths.infra %>/stack
224
+ working-directory: <%= inst.path %>/stack
225
225
  steps:
226
226
  - uses: actions/checkout@v5
227
227
  - uses: hashicorp/setup-terraform@v3
@@ -1,5 +1,5 @@
1
1
  services:
2
- <% if (components.includes('fastapi') || components.includes('fastify')) { %>
2
+ <% if (fastapiInstances.length > 0 || fastifyInstances.length > 0) { %>
3
3
  db:
4
4
  image: postgres:16-alpine
5
5
  environment:
@@ -23,9 +23,9 @@ services:
23
23
  networks:
24
24
  - app-network
25
25
  <% } %>
26
- <% if (components.includes('fastapi')) { %>
27
- <%= paths.fastapi %>-migrate:
28
- build: ./<%= paths.fastapi %>
26
+ <% for (const inst of fastapiInstances) { %>
27
+ <%= inst.path %>-migrate:
28
+ build: ./<%= inst.path %>
29
29
  command: ['uv', 'run', 'migrate.py']
30
30
  environment:
31
31
  - SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://dev:dev@db:5432/app
@@ -39,16 +39,21 @@ services:
39
39
  cpus: '0.5'
40
40
  networks:
41
41
  - app-network
42
- <%= paths.fastapi %>:
43
- build: ./<%= paths.fastapi %>
42
+ <%= inst.path %>:
43
+ build: ./<%= inst.path %>
44
44
  command:
45
45
  [
46
46
  'uv', 'run', 'uvicorn', 'src.app:app',
47
47
  '--host', '0.0.0.0', '--port', '7860',
48
48
  '--workers', '2', '--timeout-keep-alive', '120', '--reload',
49
49
  ]
50
+ <% if (inst.path === inst.type) { %>
50
51
  ports:
51
52
  - '7860:7860'
53
+ <% } else { %>
54
+ expose:
55
+ - '7860'
56
+ <% } %>
52
57
  environment:
53
58
  - SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://dev:dev@db:5432/app
54
59
  - CORS_ALLOW_ORIGINS=http://localhost:3000,http://localhost
@@ -56,11 +61,11 @@ services:
56
61
  - JWT_SECRET=dev-secret-that-is-at-least-32-bytes-long
57
62
  - JWT_ALGORITHMS=HS256
58
63
  volumes:
59
- - ./<%= paths.fastapi %>/src:/app/src
60
- - ./<%= paths.fastapi %>/alembic.ini:/app/alembic.ini
61
- - ./<%= paths.fastapi %>/migrate.py:/app/migrate.py
64
+ - ./<%= inst.path %>/src:/app/src
65
+ - ./<%= inst.path %>/alembic.ini:/app/alembic.ini
66
+ - ./<%= inst.path %>/migrate.py:/app/migrate.py
62
67
  depends_on:
63
- <%= paths.fastapi %>-migrate:
68
+ <%= inst.path %>-migrate:
64
69
  condition: service_completed_successfully
65
70
  restart: unless-stopped
66
71
  healthcheck:
@@ -81,9 +86,9 @@ services:
81
86
  networks:
82
87
  - app-network
83
88
  <% } %>
84
- <% if (components.includes('fastify')) { %>
85
- <%= paths.fastify %>-migrate:
86
- build: ./<%= paths.fastify %>
89
+ <% for (const inst of fastifyInstances) { %>
90
+ <%= inst.path %>-migrate:
91
+ build: ./<%= inst.path %>
87
92
  command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
88
93
  environment:
89
94
  - DATABASE_URL=postgresql://dev:dev@db:5432/app
@@ -97,20 +102,25 @@ services:
97
102
  cpus: '0.5'
98
103
  networks:
99
104
  - app-network
100
- <%= paths.fastify %>:
101
- build: ./<%= paths.fastify %>
105
+ <%= inst.path %>:
106
+ build: ./<%= inst.path %>
102
107
  command: ["sh", "-c", "<%= pm.runDev %>"]
108
+ <% if (inst.path === inst.type) { %>
103
109
  ports:
104
110
  - '3000:3000'
111
+ <% } else { %>
112
+ expose:
113
+ - '3000'
114
+ <% } %>
105
115
  environment:
106
116
  - DATABASE_URL=postgresql://dev:dev@db:5432/app
107
117
  - CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost
108
118
  - JWT_PROVIDER=shared_secret
109
119
  - JWT_SECRET=dev-secret-that-is-at-least-32-bytes-long
110
120
  volumes:
111
- - ./<%= paths.fastify %>/src:/app/src
121
+ - ./<%= inst.path %>/src:/app/src
112
122
  depends_on:
113
- <%= paths.fastify %>-migrate:
123
+ <%= inst.path %>-migrate:
114
124
  condition: service_completed_successfully
115
125
  restart: unless-stopped
116
126
  healthcheck:
@@ -127,25 +137,30 @@ services:
127
137
  networks:
128
138
  - app-network
129
139
  <% } %>
130
- <% if (components.includes('frontend')) { %>
131
- frontend:
140
+ <% for (const inst of frontendInstances) { %>
141
+ <%= inst.path %>:
132
142
  image: node:20-alpine
133
143
  working_dir: /app
134
144
  command: sh -c "<%= pm.install %> && <%= pm.run %> dev -- --host 0.0.0.0"
145
+ <% if (inst.path === inst.type) { %>
135
146
  ports:
136
147
  - '5173:5173'
148
+ <% } else { %>
149
+ expose:
150
+ - '5173'
151
+ <% } %>
137
152
  env_file:
138
- - ./<%= paths.frontend %>/.env
153
+ - ./<%= inst.path %>/.env
139
154
  volumes:
140
- - ./<%= paths.frontend %>:/app
141
- - frontend_node_modules:/app/node_modules
142
- <% if (components.includes('fastify')) { %>
155
+ - ./<%= inst.path %>:/app
156
+ - <%= inst.upper.toLowerCase() %>_node_modules:/app/node_modules
157
+ <% if (fastifyInstances.length > 0) { %>
143
158
  depends_on:
144
- <%= paths.fastify %>:
159
+ <%= fastifyInstances[0].path %>:
145
160
  condition: service_healthy
146
- <% } else if (components.includes('fastapi')) { %>
161
+ <% } else if (fastapiInstances.length > 0) { %>
147
162
  depends_on:
148
- <%= paths.fastapi %>:
163
+ <%= fastapiInstances[0].path %>:
149
164
  condition: service_healthy
150
165
  <% } %>
151
166
  healthcheck:
@@ -163,11 +178,11 @@ services:
163
178
  - app-network
164
179
  <% } %>
165
180
  volumes:
166
- <% if (components.includes('fastapi') || components.includes('fastify')) { %>
181
+ <% if (fastapiInstances.length > 0 || fastifyInstances.length > 0) { %>
167
182
  pgdata:
168
183
  <% } %>
169
- <% if (components.includes('frontend')) { %>
170
- frontend_node_modules:
184
+ <% for (const inst of frontendInstances) { %>
185
+ <%= inst.upper.toLowerCase() %>_node_modules:
171
186
  <% } %>
172
187
  networks:
173
188
  app-network:
@@ -1,21 +1,21 @@
1
1
  services:
2
- <% if (components.includes('fastapi')) { %>
3
- <%= paths.fastapi %>-migrate:
4
- build: ./<%= paths.fastapi %>
2
+ <% for (const inst of fastapiInstances) { %>
3
+ <%= inst.path %>-migrate:
4
+ build: ./<%= inst.path %>
5
5
  command: ["uv", "run", "migrate.py"]
6
6
  env_file:
7
- - ./<%= paths.fastapi %>/.env
7
+ - ./<%= inst.path %>/.env
8
8
  networks:
9
9
  - app-network
10
- <%= paths.fastapi %>:
11
- build: ./<%= paths.fastapi %>
10
+ <%= inst.path %>:
11
+ build: ./<%= inst.path %>
12
12
  expose:
13
13
  - "7860"
14
14
  env_file:
15
- - ./<%= paths.fastapi %>/.env
15
+ - ./<%= inst.path %>/.env
16
16
  restart: unless-stopped
17
17
  depends_on:
18
- <%= paths.fastapi %>-migrate:
18
+ <%= inst.path %>-migrate:
19
19
  condition: service_completed_successfully
20
20
  healthcheck:
21
21
  test:
@@ -32,23 +32,23 @@ services:
32
32
  networks:
33
33
  - app-network
34
34
  <% } %>
35
- <% if (components.includes('fastify')) { %>
36
- <%= paths.fastify %>-migrate:
37
- build: ./<%= paths.fastify %>
35
+ <% for (const inst of fastifyInstances) { %>
36
+ <%= inst.path %>-migrate:
37
+ build: ./<%= inst.path %>
38
38
  command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
39
39
  env_file:
40
- - ./<%= paths.fastify %>/.env
40
+ - ./<%= inst.path %>/.env
41
41
  networks:
42
42
  - app-network
43
- <%= paths.fastify %>:
44
- build: ./<%= paths.fastify %>
43
+ <%= inst.path %>:
44
+ build: ./<%= inst.path %>
45
45
  expose:
46
46
  - "3000"
47
47
  env_file:
48
- - ./<%= paths.fastify %>/.env
48
+ - ./<%= inst.path %>/.env
49
49
  restart: unless-stopped
50
50
  depends_on:
51
- <%= paths.fastify %>-migrate:
51
+ <%= inst.path %>-migrate:
52
52
  condition: service_completed_successfully
53
53
  healthcheck:
54
54
  test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
@@ -59,10 +59,10 @@ services:
59
59
  networks:
60
60
  - app-network
61
61
  <% } %>
62
- <% if (components.includes('frontend')) { %>
63
- frontend:
62
+ <% for (const inst of frontendInstances) { %>
63
+ <%= inst.path %>:
64
64
  build:
65
- context: ./<%= paths.frontend %>
65
+ context: ./<%= inst.path %>
66
66
  args:
67
67
  VITE_API_URL: ""
68
68
  ports:
@@ -71,13 +71,13 @@ services:
71
71
  volumes:
72
72
  - letsencrypt:/etc/letsencrypt
73
73
  - certbot-www:/var/www/certbot
74
- <% if (components.includes('fastify')) { %>
74
+ <% if (fastifyInstances.length > 0) { %>
75
75
  depends_on:
76
- <%= paths.fastify %>:
76
+ <%= fastifyInstances[0].path %>:
77
77
  condition: service_healthy
78
- <% } else if (components.includes('fastapi')) { %>
78
+ <% } else if (fastapiInstances.length > 0) { %>
79
79
  depends_on:
80
- <%= paths.fastapi %>:
80
+ <%= fastapiInstances[0].path %>:
81
81
  condition: service_healthy
82
82
  <% } %>
83
83
  restart: unless-stopped
@@ -89,6 +89,8 @@ services:
89
89
  start_period: 10s
90
90
  networks:
91
91
  - app-network
92
+ <% } %>
93
+ <% if (frontendInstances.length > 0) { %>
92
94
  certbot:
93
95
  image: certbot/certbot:latest
94
96
  volumes:
@@ -97,14 +99,14 @@ services:
97
99
  entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done"
98
100
  restart: unless-stopped
99
101
  depends_on:
100
- frontend:
102
+ <%= frontendInstances[0].path %>:
101
103
  condition: service_healthy
102
104
  profiles:
103
105
  - ssl
104
106
  networks:
105
107
  - app-network
106
108
  <% } %>
107
- <% if (components.includes('frontend')) { %>
109
+ <% if (frontendInstances.length > 0) { %>
108
110
  volumes:
109
111
  letsencrypt:
110
112
  certbot-www: