Skip to content

Commit 9478779

Browse files
authored
Merge pull request #202 from devforth/next
fix(CLI): create-app Docekrfile: wrap sqlite in volume for production…
2 parents 0937a51 + 269e3b4 commit 9478779

File tree

5 files changed

+61
-91
lines changed

5 files changed

+61
-91
lines changed
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
NODE_ENV=production
2-
DATABASE_URL=sqlite://.db.sqlite
3-
DATABASE_URL={{dbUrl}}
4-
{{#if prismaDbUrl}}
5-
PRISMA_DATABASE_URL={{prismaDbUrl}}
2+
DATABASE_URL={{dbUrlProd}}
3+
{{#if prismaDbUrlProd}}
4+
PRISMA_DATABASE_URL={{prismaDbUrlProd}}
65
{{/if}}

adminforth/commands/createApp/templates/readme.md.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ You have Dockerfile ready for production deployment. You can test the build with
3838

3939
```bash
4040
docker build -t {{appName}}-image .
41-
docker run -p 3500:3500 {{appName}}-image
41+
docker run -p 3500:3500 -e ADMINFORTH_SECRET=123 {{#if sqliteFile}}-v $(pwd)/db:/code/db {{/if}}{{appName}}-image
4242
```
4343

4444
To set non-sensitive environment variables in production, use `.env.prod` file.

adminforth/commands/createApp/utils.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ function generateDbUrlForPrisma(connectionString) {
100100
return connectionString.toString();
101101
}
102102

103+
function generateDbUrlForPrismaProd(connectionString) {
104+
if (connectionString.protocol.startsWith('sqlite'))
105+
return `file:/code/db/${connectionString.host}`;
106+
if (connectionString.protocol.startsWith('mongodb'))
107+
return null;
108+
return connectionString.toString();
109+
}
110+
111+
function generateDbUrlForAfProd(connectionString) {
112+
if (connectionString.protocol.startsWith('sqlite'))
113+
return `sqlite:////code/db/${connectionString.host}`;
114+
return connectionString.toString();
115+
}
116+
103117
function initialChecks(options) {
104118
return [
105119
{
@@ -128,8 +142,12 @@ async function scaffoldProject(ctx, options, cwd) {
128142
await fse.ensureDir(projectDir);
129143

130144
const connectionString = parseConnectionString(options.db);
145+
const connectionStringProd = generateDbUrlForAfProd(connectionString);
146+
131147
const provider = detectDbProvider(connectionString.protocol);
132148
const prismaDbUrl = generateDbUrlForPrisma(connectionString);
149+
const prismaDbUrlProd = generateDbUrlForPrismaProd(connectionString);
150+
133151

134152
ctx.skipPrismaSetup = !prismaDbUrl;
135153
const appName = options.appName;
@@ -151,17 +169,23 @@ async function scaffoldProject(ctx, options, cwd) {
151169
// Write templated files
152170
await writeTemplateFiles(dirname, projectDir, {
153171
dbUrl: connectionString.toString(),
172+
dbUrlProd: connectionStringProd,
154173
prismaDbUrl,
174+
prismaDbUrlProd,
155175
appName,
156176
provider,
157177
nodeMajor: parseInt(process.versions.node.split('.')[0], 10),
178+
sqliteFile: connectionString.protocol.startsWith('sqlite') ? connectionString.host : null,
158179
});
159180

160181
return projectDir; // Return the new directory path
161182
}
162183

163184
async function writeTemplateFiles(dirname, cwd, options) {
164-
const { dbUrl, prismaDbUrl, appName, provider, nodeMajor } = options;
185+
const {
186+
dbUrl, prismaDbUrl, appName, provider, nodeMajor,
187+
dbUrlProd, prismaDbUrlProd, sqliteFile
188+
} = options;
165189

166190
// Build a list of files to generate
167191
const templateTasks = [
@@ -199,12 +223,12 @@ async function writeTemplateFiles(dirname, cwd, options) {
199223
{
200224
src: '.env.prod.hbs',
201225
dest: '.env.prod',
202-
data: { dbUrl, prismaDbUrl },
226+
data: { prismaDbUrlProd, dbUrlProd },
203227
},
204228
{
205229
src: 'readme.md.hbs',
206230
dest: 'README.md',
207-
data: { dbUrl, prismaDbUrl, appName },
231+
data: { dbUrl, prismaDbUrl, appName, sqliteFile },
208232
},
209233
{
210234
// We'll write .env using the same content as .env.sample
@@ -237,7 +261,7 @@ async function writeTemplateFiles(dirname, cwd, options) {
237261
src: '.dockerignore.hbs',
238262
dest: '.dockerignore',
239263
data: {
240-
sqliteFile: detectDbProvider(dbUrl).startsWith('sqlite') ? dbUrl.split('://')[1] : null,
264+
sqliteFile,
241265
},
242266
}
243267
];

adminforth/documentation/docs/tutorial/04-deploy.md

Lines changed: 24 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,77 +5,15 @@ with `ts-node` command in any node environment.
55

66
It will start the server on configured HTTP port and you can use any proxy like Traefik/Nginx to expose it to the internet and add SSL Layer.
77

8-
## Building SPA in Docker build time
8+
## Dockerfile
99

10-
In current index.ts file you might use call to `bundleNow` method which starts building internal SPA bundle when `index.ts` started
11-
executing. SPA building generally takes from 10 seconds to minute depending on the external modules you will add into AdminForth and extended functionality you will create.
10+
If you created your AdminForth application with `adminforth create-app` command you already have a `Dockerfile` in your project.
1211

13-
To fully exclude this bundle time we recommend doing bundling in build time.
12+
You can use it to build your AdminForth application in Docker container.
1413

15-
Create file `bundleNow.ts` in the root directory of your project:
14+
> ⚠️ Please note that `Dockerfile` has `npx adminforth bundle` command which pre-bundles your AdminForth SPA
15+
> at build time. If you will remove it, your AdminForth application will still work, but will cause some downtime during app restart/redeploy because bundling will happen at runtime after you start the `index.ts` file. When `npx adminforth bundle` command is executed at build time, the call do `bundleNow()` inside of `index.ts` file will actually do nothing.
1616
17-
and put the following code:
18-
19-
```ts title='./bundleNow.ts'
20-
import { admin } from './index.js';
21-
22-
await admin.bundleNow({ hotReload: false});
23-
console.log('Bundling AdminForth done.');
24-
```
25-
26-
Now completely Remove bundleNow call from `index.ts` file:
27-
28-
```ts title='./index.ts'
29-
//diff-remove
30-
// needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime
31-
//diff-remove
32-
await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});
33-
//diff-remove
34-
console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');
35-
//diff-add
36-
if (process.env.NODE_ENV === 'development') {
37-
//diff-add
38-
await admin.bundleNow({ hotReload: true });
39-
//diff-add
40-
console.log('Bundling AdminForth done');
41-
//diff-add
42-
}
43-
```
44-
45-
In root directory create file `.dockerignore`:
46-
47-
```bash title='./.dockerignore'
48-
node_modules
49-
*.sqlite
50-
```
51-
52-
In root directory create file `Dockerfile`:
53-
54-
```Dockerfile
55-
# use the same node version which you used during dev
56-
FROM node:20-alpine
57-
WORKDIR /code/
58-
ADD package.json package-lock.json /code/
59-
RUN npm ci
60-
ADD . /code/
61-
RUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts
62-
CMD ["npm", "run", "migrateLiveAndStart"]
63-
```
64-
65-
Add `bundleNow` and `startLive` to `package.json`:
66-
67-
```ts title='./package.json'
68-
{
69-
"type": "module",
70-
"scripts": {
71-
"env": "dotenvx run -f .env.local -f .env --overload --",
72-
"start": "npm run env -- tsx watch index.ts",
73-
...
74-
//diff-add
75-
"migrateLiveAndStart": "npx --yes prisma migrate deploy && tsx index.ts"
76-
},
77-
}
78-
```
7917

8018
## Building the image
8119

@@ -89,14 +27,13 @@ And run container with:
8927

9028
```bash
9129
docker run -p 3500:3500 \
92-
-e NODE_ENV=production \
9330
-e ADMINFORTH_SECRET=CHANGEME \
94-
-e DATABASE_FILE=/code/db/db.sqlite \
95-
-e DATABASE_FILE_URL=file:/code/db/db.sqlite \
9631
-v $(pwd)/db:/code/db \
9732
myadminapp
9833
```
9934

35+
> `-v $(pwd)/db:/code/db` is needed only if you are using SQLite database.
36+
10037
Now open your browser and go to `http://localhost:3500` to see your AdminForth application running in Docker container.
10138

10239
## Adding SSL (https) to AdminForth
@@ -106,14 +43,7 @@ change 3500 port to 80 and Cloudflare will automatically add SSL layer and faste
10643

10744
However as a bonus here we will give you independent way to add free LetsEncrypt SSL layer to your AdminForth application.
10845

109-
First move all contents of your root folder (which contains index.ts and other files) to `app` folder:
110-
111-
```bash
112-
mkdir app
113-
mv {.,}* app
114-
```
115-
116-
In root directory create file `compose.yml`:
46+
In a folder which contains folder of your AdminForth application (e.g. `adminforth-app`) create a file `compose.yml`:
11747

11848
```yaml title='./compose.yml'
11949
version: '3.8'
@@ -144,12 +74,10 @@ services:
14474
- "traefik.http.routers.http-catchall.tls=false"
14575

14676
adminforth:
147-
build: ./app
77+
build: ./adminforth-app
14878
environment:
14979
- NODE_ENV=production
15080
- ADMINFORTH_SECRET=!CHANGEME! # ☝️ replace with your secret
151-
- DATABASE_FILE=/code/db/db.sqlite
152-
- DATABASE_FILE_URL=file:/code/db/db.sqlite
15381
labels:
15482
- "traefik.enable=true"
15583
- "traefik.http.routers.adminforth.tls=true"
@@ -241,3 +169,18 @@ server {
241169
}
242170
}
243171
```
172+
173+
# Environment variables best practices
174+
175+
Use `.env` file for sensitive variables like `OPENAI_API_KEY` locally.
176+
177+
Use `.env.prod` and `.env.local` for non-sensitive variables which are different for production and local environemnts (like NODE_ENV, SOME_EXTERNAL_API_BASE, etc).
178+
179+
Sensitive variables like `OPENAI_API_KEY` in production should be passed directly to docker container or use secrets from your vault.
180+
181+
182+
# If you are not using Docker
183+
184+
You can actually ship your AdminForth application without Docker as well.
185+
186+
Most important thing is remember in such case is to use `npx adminforth bundle` command after you installed node modules and before you do actual restart of your application to avoid downtimes.

adminforth/modules/codeInjector.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,11 @@ class CodeInjector implements ICodeInjector {
800800
const skipBuild = buildHash === sourcesHash;
801801
const skipExtract = messagesHash === sourcesHash;
802802

803-
803+
if (process.env.HEAVY_DEBUG) {
804+
console.log(`🪲 SPA build hash: ${buildHash}`);
805+
console.log(`🪲 SPA messages hash: ${messagesHash}`);
806+
console.log(`🪲 SPA sources hash: ${sourcesHash}`);
807+
}
804808

805809
if (!skipExtract) {
806810
await this.runNpmShell({command: 'run i18n:extract', cwd});

0 commit comments

Comments
 (0)