diff --git a/apps/api/.env b/apps/api/.env index d96796b02..28c1052f7 100644 --- a/apps/api/.env +++ b/apps/api/.env @@ -108,4 +108,5 @@ ADMIN_SNAPSHOT_QUOTA=100 ADMIN_MAX_SNAPSHOT_SIZE=100 ADMIN_VOLUME_QUOTA=0 -SKIP_USER_EMAIL_VERIFICATION=true \ No newline at end of file +SKIP_USER_EMAIL_VERIFICATION=true +DONT_SERVE_DASHBOARD=true \ No newline at end of file diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 16792dc4c..dda73ddee 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -120,6 +120,8 @@ ENV SKIP_USER_EMAIL_VERIFICATION=true ENV RUN_MIGRATIONS=true +ENV DONT_SERVE_DASHBOARD=false + HEALTHCHECK CMD [ "curl", "-f", "http://localhost:3000/api/config" ] ENTRYPOINT ["sh", "-c", "dockerd-entrypoint.sh & node dist/apps/api/main.js"] diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 184183405..4f7da3626 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -90,12 +90,22 @@ import { getPinoTransport, swapMessageAndObject } from './common/utils/pino.util cacheControl: false, }, }), - ServeStaticModule.forRoot({ - rootPath: join(__dirname, '..', 'dashboard'), - exclude: ['/api/*'], - renderPath: '/', - serveStaticOptions: { - cacheControl: false, + ServeStaticModule.forRootAsync({ + inject: [TypedConfigService], + useFactory: (configService: TypedConfigService) => { + if (configService.get('dontServeDashboard')) { + return [] + } + return [ + { + rootPath: join(__dirname, '..', 'dashboard'), + exclude: ['/api/*'], + renderPath: '/', + serveStaticOptions: { + cacheControl: false, + }, + }, + ] }, }), ThrottlerModule.forRoot([ diff --git a/apps/api/src/config/configuration.ts b/apps/api/src/config/configuration.ts index d779cd842..a4cfe20ae 100644 --- a/apps/api/src/config/configuration.ts +++ b/apps/api/src/config/configuration.ts @@ -216,6 +216,7 @@ const configuration = { volumeQuota: parseInt(process.env.ADMIN_VOLUME_QUOTA || '0', 10), }, skipUserEmailVerification: process.env.SKIP_USER_EMAIL_VERIFICATION === 'true', + dontServeDashboard: process.env.DONT_SERVE_DASHBOARD === 'true', } export { configuration } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index afd130f68..9efbb955b 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -38,7 +38,7 @@ const httpsOptions: HttpsOptions = { async function bootstrap() { if (process.env.OTEL_ENABLED === 'true') { - await otelSdk.start() + otelSdk.start() } const app = await NestFactory.create(AppModule, { bufferLogs: true, diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile new file mode 100644 index 000000000..c6de4e706 --- /dev/null +++ b/apps/dashboard/Dockerfile @@ -0,0 +1,25 @@ +FROM node:20-alpine AS builder + +RUN npm install -g corepack && corepack enable + +WORKDIR /daytona + +COPY . . + +RUN yarn + +RUN VITE_BASE_API_URL=%DAYTONA_BASE_API_URL% yarn nx build dashboard --configuration=production --nxBail=true + +FROM nginx:alpine as dashboard + +COPY --from=builder /daytona/dist/apps/dashboard /usr/share/nginx/html +COPY --from=builder /daytona/apps/dashboard/docker/nginx/default.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /daytona/apps/dashboard/docker/entrypoint.sh /entrypoint.sh + +ENV DAYTONA_BASE_API_URL="http://api:3000" + +EXPOSE 80 + +HEALTHCHECK CMD [ "wget", "-q", "--spider", "http://localhost/" ] + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/apps/dashboard/docker/entrypoint.sh b/apps/dashboard/docker/entrypoint.sh new file mode 100755 index 000000000..be8e0306f --- /dev/null +++ b/apps/dashboard/docker/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Copyright 2025 Daytona Platforms Inc. +# SPDX-License-Identifier: AGPL-3.0 + +set -e + +# Validate DAYTONA_BASE_API_URL is a well-formed URL +if ! echo "$DAYTONA_BASE_API_URL" | grep -Eq '^https?://[a-zA-Z0-9./?=_-]*$'; then + echo "Error: DAYTONA_BASE_API_URL is not a valid URL." + exit 1 +fi + +# Escape characters that could break sed replacement +escape_sed() { + printf '%s' "$1" | sed -e 's/[\/&|\\]/\\&/g' +} +DAYTONA_BASE_API_URL_ESCAPED=$(escape_sed "$DAYTONA_BASE_API_URL") + +# Replace %DAYTONA_BASE_API_URL% with actual environment variable value +find /usr/share/nginx/html -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i "s|%DAYTONA_BASE_API_URL%|${DAYTONA_BASE_API_URL_ESCAPED}|g" {} + + +# Start nginx +exec nginx -g "daemon off;" diff --git a/apps/dashboard/docker/nginx/default.conf b/apps/dashboard/docker/nginx/default.conf new file mode 100644 index 000000000..c84e7e11f --- /dev/null +++ b/apps/dashboard/docker/nginx/default.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + absolute_redirect off; + + location = / { + return 301 /dashboard; + } + + location /dashboard { + try_files $uri $uri/ /index.html; + } +} diff --git a/apps/dashboard/project.json b/apps/dashboard/project.json index 819a593b1..e68fe725e 100644 --- a/apps/dashboard/project.json +++ b/apps/dashboard/project.json @@ -17,6 +17,13 @@ "options": { "command": "cd {projectRoot} && prettier --write \"**/*.{ts,tsx,js,jsx,json,css,mjs,html}\" --config ../../.prettierrc" } - } + }, + "check-version-env": {}, + "docker": { + "options": { + "target": "dashboard" + } + }, + "push-manifest": {} } } diff --git a/apps/docs/src/content/docs/en/oss-deployment.mdx b/apps/docs/src/content/docs/en/oss-deployment.mdx index f7da6d398..8141a8bce 100644 --- a/apps/docs/src/content/docs/en/oss-deployment.mdx +++ b/apps/docs/src/content/docs/en/oss-deployment.mdx @@ -198,6 +198,7 @@ Below is a full list of environment variables with their default values: | `ADMIN_MAX_SNAPSHOT_SIZE` | number | `100` | Admin max snapshot size, used only upon initial setup | | `ADMIN_VOLUME_QUOTA` | number | `0` | Admin volume quota, used only upon initial setup | | `SKIP_USER_EMAIL_VERIFICATION` | boolean | `true` | Skip user email verification process | +| `DONT_SERVE_DASHBOARD` | boolean | `false` | Disable serving dashboard static files from API service | ### Runner