cloudron 8.0.1 → 8.1.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.
Files changed (87) hide show
  1. package/bin/cloudron +8 -12
  2. package/package.json +1 -1
  3. package/src/actions.js +104 -34
  4. package/src/appstore-actions.js +8 -56
  5. package/src/build-actions.js +4 -1
  6. package/src/templates/flask/CloudronManifest.json +25 -0
  7. package/src/templates/flask/Dockerfile +16 -0
  8. package/src/templates/flask/app/__init__.py +5 -0
  9. package/src/templates/flask/app/views.py +5 -0
  10. package/src/templates/flask/dockerignore +5 -0
  11. package/src/templates/flask/requirements.txt +1 -0
  12. package/src/templates/flask/run.py +4 -0
  13. package/src/templates/flask/start.sh +8 -0
  14. package/src/templates/flask/supervisor/uwsgi.conf +12 -0
  15. package/src/templates/flask/template.json +3 -0
  16. package/src/templates/flask/uwsgi.ini +6 -0
  17. package/src/templates/lamp/CloudronManifest.json +26 -0
  18. package/src/templates/lamp/Dockerfile +27 -0
  19. package/src/templates/lamp/apache/app.conf +17 -0
  20. package/src/templates/lamp/apache/mpm_prefork.conf +7 -0
  21. package/src/templates/lamp/dockerignore +4 -0
  22. package/src/templates/lamp/index.php +18 -0
  23. package/src/templates/lamp/start.sh +9 -0
  24. package/src/templates/lamp/template.json +3 -0
  25. package/src/templates/lamp-composer/CloudronManifest.json +26 -0
  26. package/src/templates/lamp-composer/Dockerfile +30 -0
  27. package/src/templates/lamp-composer/apache/app.conf +17 -0
  28. package/src/templates/lamp-composer/apache/mpm_prefork.conf +7 -0
  29. package/src/templates/lamp-composer/composer.json +8 -0
  30. package/src/templates/lamp-composer/dockerignore +5 -0
  31. package/src/templates/lamp-composer/public/index.php +20 -0
  32. package/src/templates/lamp-composer/start.sh +9 -0
  33. package/src/templates/lamp-composer/template.json +3 -0
  34. package/src/templates/lemp/CloudronManifest.json +26 -0
  35. package/src/templates/lemp/Dockerfile +27 -0
  36. package/src/templates/lemp/dockerignore +4 -0
  37. package/src/templates/lemp/index.php +18 -0
  38. package/src/templates/lemp/nginx/app.conf +22 -0
  39. package/src/templates/lemp/start.sh +11 -0
  40. package/src/templates/lemp/template.json +3 -0
  41. package/src/templates/nextjs/CloudronManifest.json +25 -0
  42. package/src/templates/nextjs/Dockerfile +14 -0
  43. package/src/templates/nextjs/app/layout.js +12 -0
  44. package/src/templates/nextjs/app/page.js +7 -0
  45. package/src/templates/nextjs/dockerignore +5 -0
  46. package/src/templates/nextjs/next.config.mjs +6 -0
  47. package/src/templates/nextjs/package.json +15 -0
  48. package/src/templates/nextjs/start.sh +9 -0
  49. package/src/templates/nextjs/template.json +3 -0
  50. package/src/templates/nodejs/CloudronManifest.json +25 -0
  51. package/src/templates/nodejs/Dockerfile +10 -0
  52. package/src/templates/nodejs/dockerignore +4 -0
  53. package/src/templates/nodejs/package.json +11 -0
  54. package/src/templates/nodejs/server.js +12 -0
  55. package/src/templates/nodejs/start.sh +9 -0
  56. package/src/templates/nodejs/template.json +3 -0
  57. package/src/templates/nodejs-mongodb/CloudronManifest.json +26 -0
  58. package/src/templates/nodejs-mongodb/Dockerfile +10 -0
  59. package/src/templates/nodejs-mongodb/dockerignore +4 -0
  60. package/src/templates/nodejs-mongodb/package.json +14 -0
  61. package/src/templates/nodejs-mongodb/server.js +24 -0
  62. package/src/templates/nodejs-mongodb/start.sh +9 -0
  63. package/src/templates/nodejs-mongodb/template.json +3 -0
  64. package/src/templates/rails/CloudronManifest.json +26 -0
  65. package/src/templates/rails/Dockerfile +16 -0
  66. package/src/templates/rails/Gemfile +5 -0
  67. package/src/templates/rails/config/database.yml +4 -0
  68. package/src/templates/rails/config/puma.rb +9 -0
  69. package/src/templates/rails/dockerignore +7 -0
  70. package/src/templates/rails/public/index.html +12 -0
  71. package/src/templates/rails/start.sh +14 -0
  72. package/src/templates/rails/supervisor/puma.conf +12 -0
  73. package/src/templates/rails/template.json +3 -0
  74. package/src/templates/static/CloudronManifest.json +25 -0
  75. package/src/templates/static/Dockerfile +17 -0
  76. package/src/templates/static/apache/app.conf +13 -0
  77. package/src/templates/static/apache/mpm_prefork.conf +7 -0
  78. package/src/templates/static/dockerignore +3 -0
  79. package/src/templates/static/public/index.html +11 -0
  80. package/src/templates/static/start.sh +7 -0
  81. package/src/templates/static/template.json +3 -0
  82. package/test/test.js +1 -1
  83. /package/src/templates/{CloudronManifest.appstore.json.ejs → default/CloudronManifest.json.ejs} +0 -0
  84. /package/src/templates/{CloudronManifest.json.ejs → default/CloudronManifest.minimal.json.ejs} +0 -0
  85. /package/src/templates/{Dockerfile.ejs → default/Dockerfile.ejs} +0 -0
  86. /package/src/templates/{dockerignore.ejs → default/dockerignore.ejs} +0 -0
  87. /package/src/templates/{start.sh.ejs → default/start.sh.ejs} +0 -0
package/bin/cloudron CHANGED
@@ -27,17 +27,7 @@ program.option('--server <server>', 'Cloudron domain')
27
27
  .option('--no-wait', 'Do not wait for the operation to finish');
28
28
 
29
29
  const appstoreCommand = program.command('appstore').description('Commands for publishing to the Appstore')
30
- .option('--appstore-token <token>', 'AppStore token');
31
-
32
- appstoreCommand.command('login')
33
- .description('Login to the appstore')
34
- .option('-e, --email <email>', 'Email address')
35
- .option('-p, --password <password>', 'Password (unsafe)')
36
- .action(appstoreActions.login);
37
-
38
- appstoreCommand.command('logout')
39
- .description('Logout from the appstore')
40
- .action(appstoreActions.logout);
30
+ .requiredOption('--appstore-token <token>', 'AppStore token');
41
31
 
42
32
  appstoreCommand.command('info')
43
33
  .description('List info of published app')
@@ -264,7 +254,9 @@ program.command('inspect')
264
254
 
265
255
  program.command('init')
266
256
  .description('Creates a new CloudronManifest.json and Dockerfile')
267
- .option('--appstore', 'Appstore template')
257
+ .option('--minimal', 'Minimal template')
258
+ .option('--template <name>', 'Use a project template (use --list-templates to see all)')
259
+ .option('--list-templates', 'List available templates')
268
260
  .action(actions.init);
269
261
 
270
262
  program.command('install')
@@ -281,6 +273,8 @@ program.command('install')
281
273
  .option('--debug [cmd...]', 'Enable debug mode', false)
282
274
  .option('--readonly', 'Mount filesystem readonly. Default is read/write in debug mode.')
283
275
  .option('--env <KEY=value...>', 'Set environment variables. e.g X=1 Y=2')
276
+ .option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectBuildArgs, [])
277
+ .option('-f, --file <dockerfile>', 'Name of the Dockerfile')
284
278
  .action(actions.install);
285
279
 
286
280
  program.command('list')
@@ -405,6 +399,8 @@ program.command('update')
405
399
  .option('--image <docker image>', 'Docker image')
406
400
  .option('--no-backup', 'Skip backup [false]')
407
401
  .option('--no-force', 'Match appstore id and manifest id before updating', true)
402
+ .option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectBuildArgs, [])
403
+ .option('-f, --file <dockerfile>', 'Name of the Dockerfile')
408
404
  .action(actions.update);
409
405
 
410
406
  const versionsCommand = program.command('versions').description('Commands for publishing community packages');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "8.0.1",
3
+ "version": "8.1.0",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "type": "module",
package/src/actions.js CHANGED
@@ -224,7 +224,7 @@ async function waitForFinishInstallation(appId, taskId, options) {
224
224
 
225
225
  if (response.body.installationState !== 'installed') throw new Error(`Installation failed: ${response.body.error ? response.body.error.message : ''}`);
226
226
 
227
- await waitForHealthy(appId, options);
227
+ if (!options.debug) await waitForHealthy(appId, options);
228
228
  }
229
229
 
230
230
  async function waitForFinishBackup(appId, taskId, options) {
@@ -683,7 +683,6 @@ async function install(localOptions, cmd) {
683
683
  cmd: debugCmd
684
684
  };
685
685
  data.memoryLimit = -1;
686
- options.wait = false; // in debug mode, health check never succeeds
687
686
  }
688
687
 
689
688
  if (!options.appstoreId && !options.versionsUrl && manifest && manifest.icon) {
@@ -712,6 +711,10 @@ async function install(localOptions, cmd) {
712
711
  if (data.sso !== undefined) request.field('sso', String(data.sso));
713
712
  if (data.debugMode) request.field('debugMode', JSON.stringify(data.debugMode));
714
713
  if (data.icon) request.field('icon', data.icon);
714
+ const buildConfig = {};
715
+ if (options.buildArg && options.buildArg.length) buildConfig.buildArgs = options.buildArg;
716
+ if (options.file) buildConfig.dockerfileName = options.file;
717
+ if (Object.keys(buildConfig).length) request.field('buildConfig', JSON.stringify(buildConfig));
715
718
  request.attach('sourceArchive', sourceArchiveFilePath);
716
719
 
717
720
  response = await request;
@@ -903,6 +906,10 @@ async function update(localOptions, cmd) {
903
906
  request.field('skipNotification', String(data.skipNotification));
904
907
  request.field('force', String(data.force));
905
908
  if (data.icon) request.field('icon', data.icon);
909
+ const buildConfig = {};
910
+ if (options.buildArg && options.buildArg.length) buildConfig.buildArgs = options.buildArg;
911
+ if (options.file) buildConfig.dockerfileName = options.file;
912
+ if (Object.keys(buildConfig).length) request.field('buildConfig', JSON.stringify(buildConfig));
906
913
  request.attach('sourceArchive', sourceArchiveFilePath);
907
914
 
908
915
  response = await request;
@@ -1504,7 +1511,6 @@ function runExecSession(appId, execId, { stdin, stdout, stderr, tty }, options)
1504
1511
  if (stdout !== process.stdout && typeof stdout.end === 'function') {
1505
1512
  stdout.on('close', () => resolve());
1506
1513
  stdout.end();
1507
- stdout.resume(); // drain readable side of Duplex/Transform streams so autoDestroy emits 'close'
1508
1514
  } else {
1509
1515
  setImmediate(resolve);
1510
1516
  }
@@ -1912,52 +1918,116 @@ async function syncPull(remoteDir, localDir, localOptions, cmd) {
1912
1918
  }
1913
1919
  }
1914
1920
 
1921
+ function listTemplates() {
1922
+ const templatesDir = path.join(import.meta.dirname, 'templates');
1923
+ const entries = fs.readdirSync(templatesDir, { withFileTypes: true });
1924
+
1925
+ console.log();
1926
+ console.log('Available templates:');
1927
+ console.log();
1928
+
1929
+ for (const entry of entries) {
1930
+ if (!entry.isDirectory()) continue;
1931
+
1932
+ const metaPath = path.join(templatesDir, entry.name, 'template.json');
1933
+ if (!fs.existsSync(metaPath)) continue;
1934
+
1935
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
1936
+ console.log(` ${entry.name.padEnd(15)} ${meta.description}`);
1937
+ }
1938
+
1939
+ console.log();
1940
+ }
1941
+
1942
+ function copyTemplateDir(srcDir, destDir) {
1943
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
1944
+
1945
+ for (const entry of entries) {
1946
+ const srcPath = path.join(srcDir, entry.name);
1947
+
1948
+ if (entry.name === 'template.json') continue;
1949
+
1950
+ const destName = entry.name === 'dockerignore' ? '.dockerignore' : entry.name;
1951
+ const destPath = path.join(destDir, destName);
1952
+
1953
+ if (entry.isDirectory()) {
1954
+ if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
1955
+ copyTemplateDir(srcPath, destPath);
1956
+ } else {
1957
+ if (fs.existsSync(destPath)) {
1958
+ console.log(`${destName} already exists, skipping`);
1959
+ continue;
1960
+ }
1961
+
1962
+ fs.copyFileSync(srcPath, destPath);
1963
+
1964
+ if (destName === 'start.sh') fs.chmodSync(destPath, 0o0775);
1965
+ }
1966
+ }
1967
+ }
1968
+
1915
1969
  function init(localOptions, cmd) {
1916
1970
  const options = cmd.optsWithGlobals();
1971
+
1972
+ if (options.listTemplates) return listTemplates();
1973
+
1974
+ if (options.template && options.minimal) return exit('--template and --minimal are mutually exclusive');
1975
+
1917
1976
  const manifestFilePath = locateManifest();
1918
1977
  if (manifestFilePath && path.dirname(manifestFilePath) === process.cwd()) return exit('CloudronManifest.json already exists in current directory');
1919
1978
 
1920
- const manifestTemplateFilename = options.appstore ? 'CloudronManifest.appstore.json.ejs' : 'CloudronManifest.json.ejs';
1979
+ if (options.template) {
1980
+ const templateDir = path.join(import.meta.dirname, 'templates', options.template);
1981
+ const metaPath = path.join(templateDir, 'template.json');
1982
+ if (!fs.existsSync(metaPath)) {
1983
+ console.log(`Unknown template "${options.template}".`);
1984
+ return listTemplates();
1985
+ }
1921
1986
 
1922
- const manifestTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', manifestTemplateFilename), 'utf8');
1923
- const dockerfileTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', 'Dockerfile.ejs'), 'utf8');
1924
- const dockerignoreTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', 'dockerignore.ejs'), 'utf8');
1925
- const startShTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/', 'start.sh.ejs'), 'utf8');
1987
+ copyTemplateDir(templateDir, process.cwd());
1988
+ } else {
1989
+ const manifestTemplateFilename = !options.minimal ? 'CloudronManifest.json.ejs' : 'CloudronManifest.minimal.json.ejs';
1926
1990
 
1927
- const data = {
1928
- version: '0.1.0',
1929
- title: 'App title',
1930
- httpPort: 3000
1931
- };
1991
+ const manifestTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/default/', manifestTemplateFilename), 'utf8');
1992
+ const dockerfileTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/default/', 'Dockerfile.ejs'), 'utf8');
1993
+ const dockerignoreTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/default/', 'dockerignore.ejs'), 'utf8');
1994
+ const startShTemplate = fs.readFileSync(path.join(import.meta.dirname, 'templates/default/', 'start.sh.ejs'), 'utf8');
1932
1995
 
1933
- const manifest = ejs.render(manifestTemplate, data);
1934
- fs.writeFileSync('CloudronManifest.json', manifest, 'utf8');
1996
+ const data = {
1997
+ version: '0.1.0',
1998
+ title: 'App title',
1999
+ httpPort: 3000
2000
+ };
1935
2001
 
1936
- if (fs.existsSync('Dockerfile')) {
1937
- console.log('Dockerfile already exists, skipping');
1938
- } else {
1939
- const dockerfile = ejs.render(dockerfileTemplate, data);
1940
- fs.writeFileSync('Dockerfile', dockerfile, 'utf8');
1941
- }
2002
+ const manifest = ejs.render(manifestTemplate, data);
2003
+ fs.writeFileSync('CloudronManifest.json', manifest, 'utf8');
1942
2004
 
1943
- if (fs.existsSync('.dockerignore')) {
1944
- console.log('.dockerignore already exists, skipping');
1945
- } else {
1946
- const dockerignore = ejs.render(dockerignoreTemplate, data);
1947
- fs.writeFileSync('.dockerignore', dockerignore, 'utf8');
1948
- }
2005
+ if (fs.existsSync('Dockerfile')) {
2006
+ console.log('Dockerfile already exists, skipping');
2007
+ } else {
2008
+ const dockerfile = ejs.render(dockerfileTemplate, data);
2009
+ fs.writeFileSync('Dockerfile', dockerfile, 'utf8');
2010
+ }
1949
2011
 
1950
- if (fs.existsSync('start.sh')) {
1951
- console.log('start.sh already exists, skipping');
1952
- } else {
1953
- const dockerignore = ejs.render(startShTemplate, {});
1954
- fs.writeFileSync('start.sh', dockerignore, 'utf8');
1955
- fs.chmodSync('start.sh', 0o0775);
2012
+ if (fs.existsSync('.dockerignore')) {
2013
+ console.log('.dockerignore already exists, skipping');
2014
+ } else {
2015
+ const dockerignore = ejs.render(dockerignoreTemplate, data);
2016
+ fs.writeFileSync('.dockerignore', dockerignore, 'utf8');
2017
+ }
2018
+
2019
+ if (fs.existsSync('start.sh')) {
2020
+ console.log('start.sh already exists, skipping');
2021
+ } else {
2022
+ const startSh = ejs.render(startShTemplate, {});
2023
+ fs.writeFileSync('start.sh', startSh, 'utf8');
2024
+ fs.chmodSync('start.sh', 0o0775);
2025
+ }
1956
2026
  }
1957
2027
 
1958
2028
  fs.copyFileSync(path.join(import.meta.dirname, 'templates/', 'logo.png'), 'logo.png');
1959
2029
 
1960
- if (options.appstore) {
2030
+ if (!options.minimal) {
1961
2031
  if (!fs.existsSync('.gitignore')) fs.writeFileSync('.gitignore', 'node_modules/\n', 'utf8');
1962
2032
  if (!fs.existsSync('DESCRIPTION.md')) fs.writeFileSync('DESCRIPTION.md', '## About\n\nThis app changes everything\n\n', 'utf8');
1963
2033
  if (!fs.existsSync('POSTINSTALL.md')) fs.writeFileSync('POSTINSTALL.md', 'Post installation information\n\n', 'utf8');
@@ -13,7 +13,7 @@ import Table from 'easy-table';
13
13
  const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
14
14
 
15
15
  function requestError(response) {
16
- if (response.status === 401) return 'Invalid token. Use cloudron appstore login again.';
16
+ if (response.status === 401) return 'Invalid token.';
17
17
 
18
18
  return `${response.status} message: ${response.body?.message || response.text || JSON.stringify(response.body)}`; // body is sometimes just a string like in 401
19
19
  }
@@ -30,10 +30,6 @@ function createRequest(method, apiPath, options) {
30
30
  return request;
31
31
  }
32
32
 
33
- function createUrl(api) {
34
- return config.appStoreOrigin() + api;
35
- }
36
-
37
33
  // the app argument allows us in the future to get by name or id
38
34
  async function getAppstoreId(appstoreId) {
39
35
  if (appstoreId) return appstoreId.split('@');
@@ -47,54 +43,6 @@ async function getAppstoreId(appstoreId) {
47
43
  return [manifest.id, manifest.version];
48
44
  }
49
45
 
50
- async function authenticate(options) { // maybe we can use options.token to valid using a profile call?
51
- if (!options.hideBanner) {
52
- const webDomain = config.appStoreOrigin().replace('https://api.', '');
53
- console.log(`${webDomain} login` + ` (If you do not have one, sign up at https://${webDomain}/console.html#/register)`);
54
- }
55
-
56
- const email = options.email || await readline.question('Email: ', {});
57
- const password = options.password || await readline.question('Password: ', { noEchoBack: true });
58
-
59
- config.setAppStoreToken(null);
60
-
61
- const response = await superagent.post(createUrl('/api/v1/login')).auth(email, password).send({ totpToken: options.totpToken }).ok(() => true);
62
- if (response.status === 401 && response.body.message.indexOf('TOTP') !== -1) {
63
- if (response.body.message === 'TOTP token missing') console.log('A 2FA TOTP Token is required for this account.');
64
-
65
- options.totpToken = await readline.question('2FA token: ', {});
66
- options.email = email;
67
- options.password = password;
68
- options.hideBanner = true;
69
-
70
- return await authenticate(options); // try again with top set
71
- }
72
-
73
- if (response.status !== 200) {
74
- console.log('Login failed.');
75
-
76
- options.hideBanner = true;
77
- options.email = '';
78
- options.password = '';
79
-
80
- return await authenticate(options);
81
- }
82
-
83
- config.setAppStoreToken(response.body.accessToken);
84
-
85
- console.log('Login successful.');
86
- }
87
-
88
- async function login(localOptions, cmd) {
89
- const options = cmd.optsWithGlobals();
90
- await authenticate(options);
91
- }
92
-
93
- function logout() {
94
- config.setAppStoreToken(null);
95
- console.log('Done.');
96
- }
97
-
98
46
  async function info(localOptions, cmd) {
99
47
  const options = cmd.optsWithGlobals();
100
48
  const [id, version] = await getAppstoreId(options.appstoreId);
@@ -238,19 +186,25 @@ async function verifyManifest(localOptions, cmd) {
238
186
  const appConfig = config.getCwdConfig(sourceDir);
239
187
 
240
188
  // image can be passed in options for buildbot
189
+ let missingDockerImage = false;
241
190
  if (options.image) {
242
191
  manifest.dockerImage = options.image;
243
192
  } else {
244
193
  manifest.dockerImage = appConfig.dockerImage;
245
194
  }
246
195
 
247
- if (!manifest.dockerImage) exit('No docker image found, run `cloudron build` first');
196
+ if (!manifest.dockerImage) {
197
+ missingDockerImage = true;
198
+ manifest.dockerImage = `cloudron/${manifest.id}:${manifest.version}`;
199
+ }
248
200
 
249
201
  // ensure we remove the docker hub handle
250
202
  if (manifest.dockerImage.indexOf('docker.io/') === 0) manifest.dockerImage = manifest.dockerImage.slice('docker.io/'.length);
251
203
 
252
204
  const error = manifestFormat.checkAppstoreRequirements(manifest);
253
205
  if (error) return exit(error);
206
+
207
+ if (missingDockerImage) return exit('No docker image found, run `cloudron build` first');
254
208
  }
255
209
 
256
210
  async function checkDockerHub(dockerImage) {
@@ -471,8 +425,6 @@ async function notify() {
471
425
  }
472
426
 
473
427
  export default {
474
- login,
475
- logout,
476
428
  info,
477
429
  listVersions,
478
430
  submit,
@@ -313,7 +313,10 @@ async function build(localOptions, cmd) {
313
313
  config.setCwdConfig(sourceDir, appConfig);
314
314
  }
315
315
 
316
- appConfig.gitCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); // when the build gets saved, save the gitCommit also
316
+ const gitCommit = safe.child_process.execSync('git rev-parse HEAD', { encoding: 'utf8' })?.trim(); // when the build gets saved, save the gitCommit also
317
+ if (safe.error) console.log('No git repository found. Continuing without commit sha info.');
318
+
319
+ appConfig.gitCommit = gitCommit || '';
317
320
  if (buildServiceConfig.type === 'remote' && buildServiceConfig.url) {
318
321
  console.log('Building using remote build service at %s', buildServiceConfig.url);
319
322
  await buildRemote(manifest, sourceDir, appConfig, options, buildServiceConfig);
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "",
3
+ "version": "0.1.0",
4
+ "upstreamVersion": "",
5
+ "minBoxVersion": "9.0.0",
6
+ "title": "",
7
+ "author": "",
8
+ "description": "file://DESCRIPTION.md",
9
+ "tagline": "",
10
+ "website": "",
11
+ "contactEmail": "",
12
+ "iconUrl": "file://logo.png",
13
+ "healthCheckPath": "/",
14
+ "mediaLinks": [],
15
+ "httpPort": 3000,
16
+ "tags": [],
17
+ "changelog": "file://CHANGELOG",
18
+ "postInstallMessage": "file://POSTINSTALL.md",
19
+ "documentationUrl": "",
20
+ "forumUrl": "",
21
+ "addons": {
22
+ "localstorage": {}
23
+ },
24
+ "manifestVersion": 2
25
+ }
@@ -0,0 +1,16 @@
1
+ FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c
2
+
3
+ RUN mkdir -p /app/code
4
+ WORKDIR /app/code
5
+
6
+ COPY . /app/code
7
+
8
+ RUN pip install -r requirements.txt
9
+
10
+ ADD supervisor/* /etc/supervisor/conf.d/
11
+ RUN ln -sf /run/supervisord.log /var/log/supervisor/supervisord.log
12
+
13
+ ADD uwsgi.ini /etc/uwsgi/apps-available/flask-uwsgi.ini
14
+ RUN ln -s /etc/uwsgi/apps-available/flask-uwsgi.ini /etc/uwsgi/apps-enabled/flask-uwsgi.ini
15
+
16
+ CMD ["/app/code/start.sh"]
@@ -0,0 +1,5 @@
1
+ from flask import Flask
2
+
3
+ app = Flask(__name__)
4
+
5
+ from app import views
@@ -0,0 +1,5 @@
1
+ from app import app
2
+
3
+ @app.route("/")
4
+ def index():
5
+ return "Hello from Flask"
@@ -0,0 +1,5 @@
1
+ .git
2
+ .gitignore
3
+ .dockerignore
4
+ node_modules
5
+ __pycache__
@@ -0,0 +1 @@
1
+ flask
@@ -0,0 +1,4 @@
1
+ from app import app
2
+
3
+ if __name__ == "__main__":
4
+ app.run()
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ set -eu
4
+
5
+ export FLASK_APP=run.py
6
+
7
+ echo "Starting supervisor"
8
+ exec /usr/bin/supervisord --configuration /etc/supervisor/supervisord.conf --nodaemon -i FlaskApp
@@ -0,0 +1,12 @@
1
+ [program:uwsgi]
2
+ autorestart=true
3
+ autostart=true
4
+ command=uwsgi --master --workers 2 --no-orphans --ini /etc/uwsgi/apps-enabled/flask-uwsgi.ini
5
+ stdout_logfile=/dev/stdout
6
+ stdout_logfile_maxbytes=0
7
+ stderr_logfile=/dev/stderr
8
+ stderr_logfile_maxbytes=0
9
+ killasgroup=true
10
+ stopasgroup=true
11
+ stopsignal=QUIT
12
+ priority=100
@@ -0,0 +1,3 @@
1
+ {
2
+ "description": "Python Flask app with uWSGI and Supervisor"
3
+ }
@@ -0,0 +1,6 @@
1
+ [uwsgi]
2
+ plugin = python
3
+ chdir = /app/code
4
+ uid = cloudron
5
+ gid = cloudron
6
+ http-socket = 0.0.0.0:3000
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "",
3
+ "version": "0.1.0",
4
+ "upstreamVersion": "",
5
+ "minBoxVersion": "9.0.0",
6
+ "title": "",
7
+ "author": "",
8
+ "description": "file://DESCRIPTION.md",
9
+ "tagline": "",
10
+ "website": "",
11
+ "contactEmail": "",
12
+ "iconUrl": "file://logo.png",
13
+ "healthCheckPath": "/",
14
+ "mediaLinks": [],
15
+ "httpPort": 3000,
16
+ "tags": [],
17
+ "changelog": "file://CHANGELOG",
18
+ "postInstallMessage": "file://POSTINSTALL.md",
19
+ "documentationUrl": "",
20
+ "forumUrl": "",
21
+ "addons": {
22
+ "localstorage": {},
23
+ "mysql": {}
24
+ },
25
+ "manifestVersion": 2
26
+ }
@@ -0,0 +1,27 @@
1
+ FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c
2
+
3
+ RUN mkdir -p /app/code
4
+ WORKDIR /app/code
5
+
6
+ RUN rm /etc/apache2/sites-enabled/*
7
+ RUN sed -e 's,^ErrorLog.*,ErrorLog "|/bin/cat",' -i /etc/apache2/apache2.conf
8
+ COPY apache/mpm_prefork.conf /etc/apache2/mods-available/mpm_prefork.conf
9
+
10
+ RUN a2disconf other-vhosts-access-log
11
+ COPY apache/app.conf /etc/apache2/sites-enabled/app.conf
12
+ RUN echo "Listen 3000" > /etc/apache2/ports.conf
13
+
14
+ RUN a2enmod php8.3
15
+ RUN crudini --set /etc/php/8.3/apache2/php.ini PHP upload_max_filesize 256M && \
16
+ crudini --set /etc/php/8.3/apache2/php.ini PHP upload_max_size 256M && \
17
+ crudini --set /etc/php/8.3/apache2/php.ini PHP post_max_size 256M && \
18
+ crudini --set /etc/php/8.3/apache2/php.ini PHP memory_limit 256M && \
19
+ crudini --set /etc/php/8.3/apache2/php.ini PHP max_execution_time 200 && \
20
+ crudini --set /etc/php/8.3/apache2/php.ini Session session.save_path /run/app/sessions && \
21
+ crudini --set /etc/php/8.3/apache2/php.ini Session session.gc_probability 1 && \
22
+ crudini --set /etc/php/8.3/apache2/php.ini Session session.gc_divisor 100
23
+
24
+ COPY index.php start.sh /app/code/
25
+ RUN chown -R www-data.www-data /app/code
26
+
27
+ CMD [ "/app/code/start.sh" ]
@@ -0,0 +1,17 @@
1
+ <VirtualHost *:3000>
2
+ DocumentRoot /app/code
3
+
4
+ ErrorLog "|/bin/cat"
5
+ CustomLog "|/bin/cat" combined
6
+
7
+ <Directory /app/code/>
8
+ Options +FollowSymLinks
9
+ AllowOverride All
10
+ Require all granted
11
+
12
+ <Files "config.php">
13
+ Require all denied
14
+ </Files>
15
+ </Directory>
16
+
17
+ </VirtualHost>
@@ -0,0 +1,7 @@
1
+ <IfModule mpm_prefork_module>
2
+ StartServers 2
3
+ MinSpareServers 2
4
+ MaxSpareServers 3
5
+ MaxRequestWorkers 15
6
+ MaxConnectionsPerChild 100
7
+ </IfModule>
@@ -0,0 +1,4 @@
1
+ .git
2
+ .gitignore
3
+ .dockerignore
4
+ node_modules
@@ -0,0 +1,18 @@
1
+ <?php
2
+ $host = getenv("CLOUDRON_MYSQL_HOST");
3
+ $port = getenv("CLOUDRON_MYSQL_PORT");
4
+ $username = getenv("CLOUDRON_MYSQL_USERNAME");
5
+ $password = getenv("CLOUDRON_MYSQL_PASSWORD");
6
+ $database = getenv("CLOUDRON_MYSQL_DATABASE");
7
+
8
+ echo "<h1>Hello from Cloudron!</h1>\n";
9
+
10
+ try {
11
+ $dsn = "mysql:host=$host;port=$port;dbname=$database";
12
+ $pdo = new PDO($dsn, $username, $password);
13
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
14
+ echo "<p>MySQL connection successful.</p>\n";
15
+ } catch (PDOException $e) {
16
+ echo "<p>MySQL connection failed: " . htmlspecialchars($e->getMessage()) . "</p>\n";
17
+ }
18
+ ?>
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+
3
+ set -eu
4
+
5
+ mkdir -p /run/app/sessions
6
+
7
+ APACHE_CONFDIR="" source /etc/apache2/envvars
8
+ rm -f "${APACHE_PID_FILE}"
9
+ exec /usr/sbin/apache2 -DFOREGROUND
@@ -0,0 +1,3 @@
1
+ {
2
+ "description": "LAMP app with Apache, mod_php, and MySQL"
3
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "",
3
+ "version": "0.1.0",
4
+ "upstreamVersion": "",
5
+ "minBoxVersion": "9.0.0",
6
+ "title": "",
7
+ "author": "",
8
+ "description": "file://DESCRIPTION.md",
9
+ "tagline": "",
10
+ "website": "",
11
+ "contactEmail": "",
12
+ "iconUrl": "file://logo.png",
13
+ "healthCheckPath": "/",
14
+ "mediaLinks": [],
15
+ "httpPort": 3000,
16
+ "tags": [],
17
+ "changelog": "file://CHANGELOG",
18
+ "postInstallMessage": "file://POSTINSTALL.md",
19
+ "documentationUrl": "",
20
+ "forumUrl": "",
21
+ "addons": {
22
+ "localstorage": {},
23
+ "mysql": {}
24
+ },
25
+ "manifestVersion": 2
26
+ }