diff --git a/Dockerfile b/Dockerfile index 22dd3ccd3..b5a9e2675 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,6 @@ FROM node:20.5-slim WORKDIR /app -ARG UID=1001 -ARG GID=1001 -RUN groupadd -g $GID homarr-group -RUN useradd -r -u $UID -g $GID homarr - # Define node.js environment variables ARG PORT=7575 @@ -28,10 +23,9 @@ COPY ./drizzle/migrate ./migrate COPY ./tsconfig.json ./migrate/tsconfig.json RUN mkdir /data -RUN chown -R homarr:homarr-group /data # Install dependencies -RUN apt-get update -y && apt-get install -y openssl wget +RUN apt update && apt install -y openssl wget # Move node_modules to temp location to avoid overwriting RUN mv node_modules _node_modules @@ -54,13 +48,13 @@ EXPOSE $PORT ENV PORT=${PORT} ENV DATABASE_URL "file:/data/db.sqlite" -ENV NEXTAUTH_URL "http://localhost:3000" +ENV NEXTAUTH_URL "http://localhost:7575" ENV PORT 7575 ENV NEXTAUTH_SECRET NOT_IN_USE_BECAUSE_JWTS_ARE_UNUSED HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT} || exit 1 -USER homarr - -CMD ["sh", "./scripts/run.sh"] \ No newline at end of file +VOLUME [ "/app/data/configs" ] +VOLUME [ "/data" ] +ENTRYPOINT ["sh", "./scripts/run.sh"] diff --git a/data/default.json b/data/default.json new file mode 100644 index 000000000..910d736f7 --- /dev/null +++ b/data/default.json @@ -0,0 +1,513 @@ +{ + "schemaVersion": 1, + "configProperties": { + "name": "default" + }, + "categories": [], + "wrappers": [ + { + "id": "default", + "position": 0 + } + ], + "apps": [ + { + "id": "5df743d9-5cb1-457c-85d2-64ff86855652", + "name": "Documentation", + "url": "https://homarr.dev", + "behaviour": { + "onClickUrl": "https://homarr.dev", + "externalUrl": "https://homarr.dev", + "isOpeningNewTab": true + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [ + "200" + ] + }, + "appearance": { + "iconUrl": "/imgs/logo/logo.png", + "appNameStatus": "normal", + "positionAppName": "column", + "lineClampAppName": 1 + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "md": { + "location": { + "x": 5, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 1, + "height": 2 + } + }, + "lg": { + "location": { + "x": 6, + "y": 1 + }, + "size": { + "width": 2, + "height": 2 + } + } + } + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337", + "name": "Discord", + "url": "https://discord.com/invite/aCsmEV5RgA", + "behaviour": { + "onClickUrl": "https://discord.com/invite/aCsmEV5RgA", + "isOpeningNewTab": true, + "externalUrl": "https://discord.com/invite/aCsmEV5RgA", + "tooltipDescription": "Join our Discord server! We're waiting for your ideas and feedback. " + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [ + "200" + ] + }, + "appearance": { + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png", + "appNameStatus": "normal", + "positionAppName": "row-reverse", + "lineClampAppName": 1 + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "md": { + "location": { + "x": 3, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 1, + "y": 4 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 4, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330", + "name": "Contribute", + "url": "https://github.com/ajnart/homarr", + "behaviour": { + "onClickUrl": "https://github.com/ajnart/homarr", + "externalUrl": "https://github.com/ajnart/homarr", + "isOpeningNewTab": true, + "tooltipDescription": "" + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [] + }, + "appearance": { + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png", + "appNameStatus": "normal", + "positionAppName": "row-reverse", + "lineClampAppName": 2 + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "md": { + "location": { + "x": 3, + "y": 2 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "sm": { + "location": { + "x": 1, + "y": 3 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990", + "name": "Donate", + "url": "https://ko-fi.com/ajnart", + "behaviour": { + "onClickUrl": "https://ko-fi.com/ajnart", + "externalUrl": "https://ko-fi.com/ajnart", + "isOpeningNewTab": true, + "tooltipDescription": "Please consider making a donation" + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [ + "200" + ] + }, + "appearance": { + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png", + "appNameStatus": "normal", + "positionAppName": "row-reverse", + "lineClampAppName": 1 + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "md": { + "location": { + "x": 4, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 2, + "y": 4 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 6, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + } + ], + "widgets": [ + { + "id": "e3004052-6b83-480e-b458-56e8ccdca5f0", + "type": "weather", + "properties": { + "displayInFahrenheit": false, + "location": { + "name": "Paris", + "latitude": 48.85341, + "longitude": 2.3488 + }, + "displayCityName": true + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "md": { + "location": { + "x": 5, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + }, + { + "id": "971aa859-8570-49a1-8d34-dd5c7b3638d1", + "type": "date", + "properties": { + "display24HourFormat": true, + "dateFormat": "hide", + "enableTimezone": false, + "timezoneLocation": { + "name": "Paris", + "latitude": 48.85341, + "longitude": 2.3488 + }, + "titleState": "city" + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "sm": { + "location": { + "x": 1, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "md": { + "location": { + "x": 4, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 8, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + }, + { + "id": "f252768d-9e69-491b-b6b4-8cad04fa30e8", + "type": "date", + "properties": { + "display24HourFormat": true, + "dateFormat": "hide", + "enableTimezone": true, + "timezoneLocation": { + "name": "Tokyo", + "latitude": 35.6895, + "longitude": 139.69171 + }, + "titleState": "city" + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "md": { + "location": { + "x": 3, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 8, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + }, + { + "id": "86b1921f-efa7-410f-92dd-79553bf3264d", + "type": "notebook", + "properties": { + "showToolbar": true, + "content": "

Welcome to Homarr 🚀👋

We're glad that you're here! Homarr is a modern and easy to use dashboard that helps you to organize and manage your home network from one place. Control is at your fingertips.

We recommend you to read the getting started guide first. To edit this board you must enter the edit mode - only administrators can do this. Adding an app is the first step you should take. You can do this by clicking the Add tile button at the top right and select App. After you provided an internal URL, external URL and selected an icon you can drag it around when holding down the left mouse button. Make it bigger or smaller using the drag icon at the bottom right. When you're happy with it's position, you must exit edit mode to save your board. Adding widgets works the same way but may require additional configuration - read the documentation for more information.

To remove this widget, you must log in to your administrator account and click on the menu to delete it.

Your TODO list:

" + }, + "area": { + "type": "wrapper", + "properties": { + "id": "default" + } + }, + "shape": { + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 3, + "height": 2 + } + }, + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 3, + "height": 4 + } + }, + "lg": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 6, + "height": 3 + } + } + } + } + ], + "settings": { + "common": { + "searchEngine": { + "type": "google", + "properties": {} + } + }, + "customization": { + "layout": { + "enabledLeftSidebar": false, + "enabledRightSidebar": false, + "enabledDocker": false, + "enabledPing": false, + "enabledSearchbar": true + }, + "pageTitle": "Homarr ⭐️", + "logoImageUrl": "/imgs/logo/logo.png", + "faviconUrl": "/imgs/favicon/favicon-squared.png", + "backgroundImageUrl": "", + "customCss": "", + "colors": { + "primary": "red", + "secondary": "yellow", + "shade": 7 + }, + "appOpacity": 100, + "gridstack": { + "columnCountSmall": 3, + "columnCountMedium": 6, + "columnCountLarge": 10 + } + }, + "access": { + "allowGuests": false + } + } +} \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh index 01e80d0e8..67c5cb508 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -9,4 +9,6 @@ cd ./migrate; yarn db:migrate & PID=$! wait $PID echo "Starting production server..." -node /app/server.js \ No newline at end of file +node /app/server.js & PID=$! + +wait $PID \ No newline at end of file diff --git a/src/components/Onboarding/database-not-writeable.tsx b/src/components/Onboarding/database-not-writeable.tsx new file mode 100644 index 000000000..ac1e92527 --- /dev/null +++ b/src/components/Onboarding/database-not-writeable.tsx @@ -0,0 +1,42 @@ +import { Center, Code, List, Stack, Text, Title } from '@mantine/core'; +import Head from 'next/head'; + +export const DatabaseNotWriteable = ({ error, errorMessage }: { error: any | unknown, errorMessage: string | undefined }) => { + return ( + <> + + Onboard - Error • Homarr + + +
+ + + Critical error while starting Homarr + + + We detected that Homarr is unable to write to the database. Please troubleshoot using + the following steps: + + + + Ensure that you mounted the path /data to a writeable location with + enough disk space. For this, you must add the following mounting point to your docker + compose: {' - /data:/data'} + + + Ensure that you followed the installation instructions at{' '} + + https://homarr.dev/docs/introduction/installation + + + + {error && JSON.stringify(error)} + + {errorMessage && ( + {errorMessage} + )} + +
+ + ); +}; diff --git a/src/middleware.ts b/src/middleware.ts index 63e2b45d0..ee139deca 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -47,11 +47,5 @@ const shouldRedirectToOnboard = async (): Promise => { return cachedUserCount === 0; }; - if (!process.env.DATABASE_URL?.startsWith('file:')) { - return await cacheAndGetUserCount(); - } - - const fileUri = process.env.DATABASE_URL.substring(4); return await cacheAndGetUserCount(); - // TODO: Show an error page if the database file is read-only }; diff --git a/src/pages/onboard.tsx b/src/pages/onboard.tsx index 01c248131..ab7b4b55d 100644 --- a/src/pages/onboard.tsx +++ b/src/pages/onboard.tsx @@ -1,19 +1,28 @@ -import { Box, Button, Center, Image, Stack, Text, Title, useMantineTheme } from '@mantine/core'; +import { Button, Center, Image, Stack, Text, Title, useMantineTheme } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconArrowRight } from '@tabler/icons-react'; +import Consola from 'consola'; import fs from 'fs'; +import fsPromises from 'fs/promises'; import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import Head from 'next/head'; +import { DatabaseNotWriteable } from '~/components/Onboarding/database-not-writeable'; import { OnboardingSteps } from '~/components/Onboarding/onboarding-steps'; import { ThemeSchemeToggle } from '~/components/ThemeSchemeToggle/ThemeSchemeToggle'; import { FloatingBackground } from '~/components/layout/Background/FloatingBackground'; -import { db } from '~/server/db'; +import { env } from '~/env'; import { getTotalUserCountAsync } from '~/server/db/queries/user'; import { getConfig } from '~/tools/config/getConfig'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +const util = require('util'); +const exec = util.promisify(require('child_process').exec); + export default function OnboardPage({ configSchemaVersions, + databaseNotWriteable, + error, + errorMessage }: InferGetServerSidePropsType) { const { fn, colors, colorScheme } = useMantineTheme(); const background = colorScheme === 'dark' ? 'dark.6' : 'gray.1'; @@ -39,29 +48,35 @@ export default function OnboardPage({ - {onboardingSteps ? ( - + {databaseNotWriteable == true ? ( + ) : ( -
- - - Welcome to Homarr! - - - Your favorite dashboard has received a big upgrade. -
- We'll help you update within the next few steps -
+ <> + {onboardingSteps ? ( + + ) : ( +
+ + + Welcome to Homarr! + + + Your favorite dashboard has received a big upgrade. +
+ We'll help you update within the next few steps +
- -
-
+ +
+
+ )} + )} @@ -87,10 +102,65 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { ctx.res ); + if (env.DATABASE_URL.startsWith('file:')) { + const rawDatabaseUrl = env.DATABASE_URL.substring('file:'.length); + Consola.info( + `Instance is using a database on the file system. Checking if file '${rawDatabaseUrl}' is writable...` + ); + try { + await fsPromises.access(rawDatabaseUrl, fs.constants.W_OK); + } catch (error) { + // this usually occurs when the database path is not mounted in Docker + Consola.error(`Database '${rawDatabaseUrl}' is not writable.`, error); + return { + props: { + ...translations, + configSchemaVersions: configSchemaVersions, + databaseNotWriteable: true, + error: error, + }, + }; + } + Consola.info('Database is writeable'); + + if (process.platform !== 'win32') { + try { + const { stdout, stderr } = await exec("mount | grep '/data'"); + + if (stderr.split('\n').length > 1 || stdout.split('\n').length <= 1) { + Consola.error(`Database at '${rawDatabaseUrl}' has not been mounted: ${stdout.replace('\n', '\\n')} ${stderr.replace('\n', '\\n')}`); + return { + props: { + ...translations, + configSchemaVersions: configSchemaVersions, + databaseNotWriteable: true, + error: `Database at '${rawDatabaseUrl}' is not mounted:\n${stdout}`, + }, + }; + } + } catch (error) { + const errorMessage = `Database at '${rawDatabaseUrl}' has not been mounted: ${error}`; + Consola.error(errorMessage); + return { + props: { + ...translations, + configSchemaVersions: configSchemaVersions, + databaseNotWriteable: true, + error: error, + errorMessage: errorMessage + }, + }; + } + } + + Consola.info(`Database at '${rawDatabaseUrl}' is writeable and mounted`); + } + return { props: { ...translations, configSchemaVersions: configSchemaVersions, + databaseNotWriteable: false }, }; }; diff --git a/src/tools/config/getFallbackConfig.ts b/src/tools/config/getFallbackConfig.ts index cbb760743..dbc74888e 100644 --- a/src/tools/config/getFallbackConfig.ts +++ b/src/tools/config/getFallbackConfig.ts @@ -1,6 +1,6 @@ import { ConfigType } from '~/types/config'; -import defaultConfig from '../../../data/configs/default.json'; +import defaultConfig from '../../../data/default.json'; export const getFallbackConfig = (name?: string) => ({ ...defaultConfig,