Feat: Dockerizing using nginx reverse proxy (#280)

* minor docker fixes

* eslint config changes

* dockerfile changes to backend and frontend

* oauth enabled env flag

* sentry enabled env flag

* build: get alternatives for environment variables and static file storage

* build: automatically generate random secret key if not provided

* build: update docker compose for next url env add channels to requirements for asgi server and save files in local machine for docker environment

* build: update nginx conf for backend base url update backend dockerfile to make way for static file uploads

* feat: create a default user with given values else default values

* chore: update docker python version and other dependency version in docker

* build: update local settings file to run it in docker

* fix: update script to run in default production setting

* fix: env variable changes and env setup shell script added

* Added Single Dockerfile to run the Entire plane application

* docs build fixes

---------

Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
sriram veeraghanta 2023-02-21 11:31:43 +05:30 committed by GitHub
parent 33e2986062
commit bdca84bd09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 9613 additions and 11018 deletions

View File

@ -1,10 +1,10 @@
module.exports = {
root: true,
// This tells ESLint to load the config from the package `config`
// extends: ["custom"],
// This tells ESLint to load the config from the package `eslint-config-custom`
extends: ["custom"],
settings: {
next: {
rootDir: ["apps/*/"],
rootDir: ["apps/*"],
},
},
};

5
.gitignore vendored
View File

@ -65,3 +65,8 @@ package-lock.json
# Sentry
.sentryclirc
# lock files
package-lock.json
pnpm-lock.yaml
pnpm-workspace.yaml

116
Dockerfile Normal file
View File

@ -0,0 +1,116 @@
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN yarn turbo run build --filter=app
FROM python:3.11.1-alpine3.17 AS backend
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /code
RUN apk --update --no-cache add \
"libpq~=15" \
"libxslt~=1.1" \
"nodejs-current~=19" \
"xmlsec~=1.2" \
"nginx" \
"nodejs" \
"npm" \
"supervisor"
COPY apiserver/requirements.txt ./
COPY apiserver/requirements ./requirements
RUN apk add libffi-dev
RUN apk --update --no-cache --virtual .build-deps add \
"bash~=5.2" \
"g++~=12.2" \
"gcc~=12.2" \
"cargo~=1.64" \
"git~=2" \
"make~=4.3" \
"postgresql13-dev~=13" \
"libc-dev" \
"linux-headers" \
&& \
pip install -r requirements.txt --compile --no-cache-dir \
&& \
apk del .build-deps
# Add in Django deps and generate Django's static files
COPY apiserver/manage.py manage.py
COPY apiserver/plane plane/
COPY apiserver/templates templates/
COPY apiserver/gunicorn.config.py ./
RUN apk --update --no-cache add "bash~=5.2"
COPY apiserver/bin ./bin/
RUN chmod +x ./bin/takeoff ./bin/worker
RUN chmod -R 777 /code
# Expose container port and run entry point script
EXPOSE 8000
EXPOSE 3000
EXPOSE 80
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 plane
RUN adduser --system --uid 1001 captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf
#######################################################################
COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
#######################################################################
COPY nginx/supervisor.conf /code/supervisor.conf
CMD ["supervisord","-c","/code/supervisor.conf"]

View File

@ -1,18 +1,22 @@
# Backend
SECRET_KEY="<-- django secret -->"
DJANGO_SETTINGS_MODULE="plane.settings.production"
# Database
DATABASE_URL=postgres://plane:plane@plane-db-1:5432/plane
# Cache
REDIS_URL=redis://redis:6379/
# SMPT
EMAIL_HOST="<-- email smtp -->"
EMAIL_HOST_USER="<-- email host user -->"
EMAIL_HOST_PASSWORD="<-- email host password -->"
# AWS
AWS_REGION="<-- aws region -->"
AWS_ACCESS_KEY_ID="<-- aws access key -->"
AWS_SECRET_ACCESS_KEY="<-- aws secret acess key -->"
AWS_S3_BUCKET_NAME="<-- aws s3 bucket name -->"
SENTRY_DSN="<-- sentry dsn -->"
WEB_URL="<-- frontend web url -->"
# FE
WEB_URL="localhost/"
# OAUTH
GITHUB_CLIENT_SECRET="<-- github secret -->"
# Flags
DISABLE_COLLECTSTATIC=1
DOCKERIZED=0 //True if running docker compose else 0
DOCKERIZED=1

View File

@ -1,4 +1,4 @@
FROM python:3.8.14-alpine3.16 AS backend
FROM python:3.11.1-alpine3.17 AS backend
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
@ -8,19 +8,19 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /code
RUN apk --update --no-cache add \
"libpq~=14" \
"libpq~=15" \
"libxslt~=1.1" \
"nodejs-current~=18" \
"nodejs-current~=19" \
"xmlsec~=1.2"
COPY requirements.txt ./
COPY requirements ./requirements
RUN apk add libffi-dev
RUN apk --update --no-cache --virtual .build-deps add \
"bash~=5.1" \
"g++~=11.2" \
"gcc~=11.2" \
"cargo~=1.60" \
"bash~=5.2" \
"g++~=12.2" \
"gcc~=12.2" \
"cargo~=1.64" \
"git~=2" \
"make~=4.3" \
"postgresql13-dev~=13" \
@ -46,15 +46,16 @@ COPY templates templates/
COPY gunicorn.config.py ./
USER root
RUN apk --update --no-cache add "bash~=5.1"
RUN apk --update --no-cache add "bash~=5.2"
COPY ./bin ./bin/
RUN chmod +x ./bin/takeoff ./bin/worker
RUN chmod -R 777 /code
USER captain
# Expose container port and run entry point script
EXPOSE 8000
CMD [ "./bin/takeoff" ]
# CMD [ "./bin/takeoff" ]

View File

@ -2,4 +2,8 @@
set -e
python manage.py wait_for_db
python manage.py migrate
# Create a Default User
python bin/user_script.py
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --config gunicorn.config.py --max-requests 1200 --max-requests-jitter 1000 --access-logfile -

View File

@ -0,0 +1,28 @@
import os, sys
import uuid
sys.path.append("/code")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
import django
django.setup()
from plane.db.models import User
def populate():
default_email = os.environ.get("DEFAULT_EMAIL", "captain@plane.so")
default_password = os.environ.get("DEFAULT_PASSWORD", "password123")
if not User.objects.filter(email=default_email).exists():
user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
user.set_password(default_password)
user.save()
print("User created")
print("Success")
if __name__ == "__main__":
populate()

View File

@ -1,12 +1,13 @@
import os
import datetime
from datetime import timedelta
from django.core.management.utils import get_random_secret_key
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = os.environ.get("SECRET_KEY")
SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

View File

@ -2,6 +2,7 @@
from __future__ import absolute_import
import dj_database_url
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
@ -24,6 +25,10 @@ DATABASES = {
}
}
DOCKERIZED = os.environ.get("DOCKERIZED", False)
if DOCKERIZED:
DATABASES["default"] = dj_database_url.config()
CACHES = {
"default": {
@ -41,7 +46,8 @@ INTERNAL_IPS = ("127.0.0.1",)
CORS_ORIGIN_ALLOW_ALL = True
sentry_sdk.init(
if os.environ.get("SENTRY_DSN", False):
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
integrations=[DjangoIntegration(), RedisIntegration()],
# If you wish to associate users to errors (assuming you are using
@ -49,7 +55,7 @@ sentry_sdk.init(
send_default_pii=True,
environment="local",
traces_sample_rate=0.7,
)
)
REDIS_HOST = "localhost"
REDIS_PORT = 6379
@ -64,5 +70,10 @@ RQ_QUEUES = {
},
}
WEB_URL = "http://localhost:3000"
MEDIA_URL = "/uploads/"
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads")
if DOCKERIZED:
REDIS_URL = os.environ.get("REDIS_URL")
WEB_URL = os.environ.get("WEB_URL", "localhost:3000")

View File

@ -33,6 +33,10 @@ CORS_ORIGIN_WHITELIST = [
DATABASES["default"] = dj_database_url.config()
SITE_ID = 1
DOCKERIZED = os.environ.get(
"DOCKERIZED", False
) # Set the variable true if running in docker-compose environment
# Enable Connection Pooling (if desired)
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
@ -48,99 +52,110 @@ CORS_ALLOW_ALL_ORIGINS = True
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
if os.environ.get("SENTRY_DSN", False):
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN", ""),
integrations=[DjangoIntegration(), RedisIntegration()],
# If you wish to associate users to errors (assuming you are using
# django.contrib.auth) you may enable sending PII data.
traces_sample_rate=1,
send_default_pii=True,
environment="production",
)
)
# The AWS region to connect to.
AWS_REGION = os.environ.get("AWS_REGION")
if (
os.environ.get("AWS_REGION", False)
and os.environ.get("AWS_ACCESS_KEY_ID", False)
and os.environ.get("AWS_SECRET_ACCESS_KEY", False)
and os.environ.get("AWS_S3_BUCKET_NAME", False)
):
# The AWS region to connect to.
AWS_REGION = os.environ.get("AWS_REGION", "")
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
# The optional AWS session token to use.
# AWS_SESSION_TOKEN = ""
# The optional AWS session token to use.
# AWS_SESSION_TOKEN = ""
# The name of the bucket to store files in.
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "")
# The name of the bucket to store files in.
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
# How to construct S3 URLs ("auto", "path", "virtual").
AWS_S3_ADDRESSING_STYLE = "auto"
# How to construct S3 URLs ("auto", "path", "virtual").
AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = ""
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = ""
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
# and their permissions will be set to "public-read".
AWS_S3_BUCKET_AUTH = False
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
# and their permissions will be set to "public-read".
AWS_S3_BUCKET_AUTH = False
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
# is True. It also affects the "Cache-Control" header of the files.
# Important: Changing this setting will not affect existing files.
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
# is True. It also affects the "Cache-Control" header of the files.
# Important: Changing this setting will not affect existing files.
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
# cannot be used with `AWS_S3_BUCKET_AUTH`.
AWS_S3_PUBLIC_URL = ""
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
# cannot be used with `AWS_S3_BUCKET_AUTH`.
AWS_S3_PUBLIC_URL = ""
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
# understand the consequences before enabling.
# Important: Changing this setting will not affect existing files.
AWS_S3_REDUCED_REDUNDANCY = False
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
# understand the consequences before enabling.
# Important: Changing this setting will not affect existing files.
AWS_S3_REDUCED_REDUNDANCY = False
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_DISPOSITION = ""
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_DISPOSITION = ""
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_LANGUAGE = ""
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_LANGUAGE = ""
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_METADATA = {}
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_METADATA = {}
# If True, then files will be stored using AES256 server-side encryption.
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
# Otherwise, server-side encryption is not be enabled.
# Important: Changing this setting will not affect existing files.
AWS_S3_ENCRYPT_KEY = False
# If True, then files will be stored using AES256 server-side encryption.
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
# Otherwise, server-side encryption is not be enabled.
# Important: Changing this setting will not affect existing files.
AWS_S3_ENCRYPT_KEY = False
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
# compressed size is smaller than their uncompressed size.
# Important: Changing this setting will not affect existing files.
AWS_S3_GZIP = True
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
# compressed size is smaller than their uncompressed size.
# Important: Changing this setting will not affect existing files.
AWS_S3_GZIP = True
# The signature version to use for S3 requests.
AWS_S3_SIGNATURE_VERSION = None
# The signature version to use for S3 requests.
AWS_S3_SIGNATURE_VERSION = None
# If True, then files with the same name will overwrite each other. By default it's set to False to have
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
# If True, then files with the same name will overwrite each other. By default it's set to False to have
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
# AWS Settings End
# AWS Settings End
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
else:
MEDIA_URL = "/uploads/"
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads")
# Enable Connection Pooling (if desired)
@ -155,7 +170,6 @@ ALLOWED_HOSTS = [
]
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
@ -165,7 +179,18 @@ CSRF_COOKIE_SECURE = True
REDIS_URL = os.environ.get("REDIS_URL")
CACHES = {
if DOCKERIZED:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
else:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
@ -174,7 +199,7 @@ CACHES = {
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
},
}
}
}
RQ_QUEUES = {
"default": {
@ -183,10 +208,4 @@ RQ_QUEUES = {
}
url = urlparse(os.environ.get("REDIS_URL"))
DOCKERIZED = os.environ.get(
"DOCKERIZED", False
) # Set the variable true if running in docker-compose environment
WEB_URL = os.environ.get("WEB_URL")

6
apps/app/.env.example Normal file
View File

@ -0,0 +1,6 @@
NEXT_PUBLIC_API_BASE_URL = "localhost/"
NEXT_PUBLIC_GOOGLE_CLIENTID="<-- google client id -->"
NEXT_PUBLIC_GITHUB_ID="<-- github client id -->"
NEXT_PUBLIC_SENTRY_DSN="<-- sentry dns -->"
NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_SENTRY=0

View File

@ -1 +1,4 @@
module.exports = require("config/.eslintrc");
module.exports = {
root: true,
extends: ["custom"],
};

12
apps/app/Dockerfile.dev Normal file
View File

@ -0,0 +1,12 @@
FROM node:18-alpine
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
EXPOSE 3000
CMD ["yarn","dev"]

View File

@ -4,33 +4,14 @@ RUN apk update
# Set working directory
WORKDIR /app
RUN apk add curl
RUN yarn global add turbo
COPY . .
RUN curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linuxstatic-x64" -o /bin/pnpm; chmod +x /bin/pnpm;
ENV PNPM_HOME="pnpm"
ENV PATH="${PATH}:./pnpm"
COPY ./apps ./apps
COPY ./package.json ./package.json
COPY ./.eslintrc.js ./.eslintrc.js
COPY ./turbo.json ./turbo.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm add -g turbo
RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add curl
RUN curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linuxstatic-x64" -o /bin/pnpm; chmod +x /bin/pnpm;
ENV PNPM_HOME="pnpm"
ENV PATH="${PATH}:./pnpm"
RUN apk add --no-cache libc6-compat
RUN apk update
@ -39,14 +20,14 @@ WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN pnpm turbo run build --filter=app...
RUN yarn turbo run build --filter=app
FROM node:18-alpine AS runner
WORKDIR /app
@ -62,8 +43,9 @@ COPY --from=installer /app/apps/app/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
# COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone/node_modules ./apps/app/node_modules
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
EXPOSE 3000
ENV NEXT_TELEMETRY_DISABLED 1
CMD node apps/app/server.js
EXPOSE 3000

View File

@ -12,9 +12,13 @@ const nextConfig = {
],
},
output: "standalone",
experimental: {
// this includes files from the monorepo base two directories up
outputFileTracingRoot: path.join(__dirname, "../../"),
},
};
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
if (process.env.NEXT_PUBLIC_ENABLE_SENTRY) {
module.exports = withSentryConfig(nextConfig, { silent: true }, { hideSourceMaps: true });
} else {
module.exports = nextConfig;

View File

@ -46,12 +46,12 @@
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"autoprefixer": "^10.4.7",
"config": "workspace:*",
"eslint-config-custom": "*",
"eslint": "^8.31.0",
"eslint-config-next": "12.2.2",
"postcss": "^8.4.14",
"tailwindcss": "^3.1.6",
"tsconfig": "workspace:*",
"tsconfig": "*",
"typescript": "4.7.4"
}
}

View File

@ -11,7 +11,12 @@ import authenticationService from "services/authentication.service";
// layouts
import DefaultLayout from "layouts/default-layout";
// social button
import { GoogleLoginButton, GithubLoginButton, EmailSignInForm } from "components/account";
import {
GoogleLoginButton,
GithubLoginButton,
EmailSignInForm,
EmailPasswordForm,
} from "components/account";
// ui
import { Spinner } from "components/ui";
// icons
@ -19,8 +24,6 @@ import Logo from "public/logo-with-text.png";
// types
import type { NextPage } from "next";
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
const SignInPage: NextPage = () => {
// router
const router = useRouter();
@ -69,7 +72,7 @@ const SignInPage: NextPage = () => {
.socialAuth({
medium: "github",
credential,
clientId: NEXT_PUBLIC_GITHUB_ID,
clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
})
.then(async () => {
await onSignInSuccess();
@ -109,6 +112,8 @@ const SignInPage: NextPage = () => {
Sign in to your account
</h2>
<div className="mt-16 bg-white py-8 px-4 sm:rounded-lg sm:px-10">
{Boolean(process.env.NEXT_PUBLIC_ENABLE_OAUTH) ? (
<>
<div className="mb-4">
<EmailSignInForm handleSuccess={onSignInSuccess} />
</div>
@ -118,6 +123,14 @@ const SignInPage: NextPage = () => {
<div className="mb-4">
<GithubLoginButton handleSignIn={handleGithubSignIn} />
</div>
</>
) : (
<>
<div className="mb-4">
<EmailPasswordForm onSuccess={onSignInSuccess} />
</div>
</>
)}
</div>
</div>
</div>

View File

@ -1,7 +1,4 @@
const defaultConfig = require("config/postcss.config");
module.exports = {
...defaultConfig,
plugins: {
tailwindcss: {},
autoprefixer: {},

View File

@ -1,9 +1,4 @@
/** @type {import('tailwindcss').Config} */
const defaultConfig = require("config/tailwind.config");
module.exports = {
...defaultConfig,
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
theme: {
extend: {

View File

@ -1,27 +1,3 @@
// {
// "compilerOptions": {
// "baseUrl": ".",
// "target": "es5",
// "lib": ["dom", "dom.iterable", "esnext"],
// "allowJs": true,
// "skipLibCheck": true,
// "strict": true,
// "forceConsistentCasingInFileNames": true,
// "noEmit": true,
// "esModuleInterop": true,
// "module": "esnext",
// "moduleResolution": "node",
// "resolveJsonModule": true,
// "isolatedModules": true,
// "jsx": "preserve",
// "paths": {
// "@styles/*": ["styles/*"],
// },
// "incremental": true
// },
// "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
// "exclude": ["node_modules"]
// }
{
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],

View File

@ -1 +1,4 @@
module.exports = require('config/.eslintrc')
module.exports = {
root: true,
extends: ['custom'],
}

View File

@ -38,6 +38,7 @@
"zustand": "^4.1.4"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.51.0",
"eslint": "8.26.0",
"eslint-config-next": "13.0.2",
"prettier": "^2.7.1",

View File

@ -2,6 +2,7 @@ import { Guides } from '@/components/Guides'
import { Resources } from '@/components/Resources'
import { HeroPattern } from '@/components/HeroPattern'
import { Heading } from '@/components/Heading'
import { Button } from '@/components/Button'
export const description = '.'

View File

@ -1,4 +1,5 @@
import { Heading } from '@/components/Heading'
import { Note } from '@/components/mdx'
# Project setup

View File

@ -1,3 +1,5 @@
import { Note } from '@/components/mdx'
# Get Started
This section of the Plane docs helps you get comfortable with the product and find your way around more effectively.

View File

@ -1,6 +1,7 @@
# Self Hosting Plane
import { Heading } from '@/components/Heading'
import { Note } from '@/components/mdx'
<Note>
Plane is still in its early days, not everything will be perfect yet, and

View File

@ -1,4 +1,5 @@
import { Heading } from '@/components/Heading'
import { Note } from '@/components/mdx'
# Workspace setup

View File

@ -1,8 +1,19 @@
version: "3.8"
services:
nginx:
container_name: nginx
build:
context: ./nginx
dockerfile: Dockerfile
ports:
- 80:80
depends_on:
# - plane-web
- plane-api
db:
image: postgres:12-alpine
container_name: db
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
@ -12,31 +23,28 @@ services:
POSTGRES_PASSWORD: plane
command: postgres -c 'max_connections=1000'
ports:
- "5432:5432"
- 5432:5432
redis:
image: redis:6.2.7-alpine
container_name: redis
restart: always
ports:
- "6379:6379"
- 6379:6379
volumes:
- redisdata:/data
plane-web:
image: plane-web
container_name: plane-frontend
container_name: planefrontend
build:
context: .
dockerfile: ./apps/app/Dockerfile.web
restart: always
command: node apps/app/server.js
env_file:
- ./apps/app/.env
ports:
- 3000:3000
plane-api:
image: plane-api
container_name: plane-backend
container_name: planebackend
build:
context: ./apiserver
dockerfile: Dockerfile.api
@ -45,7 +53,6 @@ services:
- 8000:8000
env_file:
- ./apiserver/.env
depends_on:
- db
- redis
@ -53,10 +60,11 @@ services:
links:
- db:db
- redis:redis
plane-worker:
image: plane-api
container_name: plane-rqworker
container_name: planerqworker
build:
context: ./apiserver
dockerfile: Dockerfile.api
depends_on:
- redis
- db
@ -67,7 +75,6 @@ services:
- db:db
env_file:
- ./apiserver/.env
volumes:
pgdata:
redisdata:

4
nginx/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

View File

@ -0,0 +1,25 @@
upstream plane {
server localhost:80;
}
error_log /var/log/nginx/error.log;
server {
listen 80;
root /www/data/;
access_log /var/log/nginx/access.log;
location / {
proxy_pass http://localhost:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/ {
proxy_pass http://localhost:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

25
nginx/nginx.conf Normal file
View File

@ -0,0 +1,25 @@
upstream plane {
server localhost:80;
}
error_log /var/log/nginx/error.log;
server {
listen 80;
root /www/data/;
access_log /var/log/nginx/access.log;
location / {
proxy_pass http://planefrontend:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/ {
proxy_pass http://planebackend:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

24
nginx/supervisor.conf Normal file
View File

@ -0,0 +1,24 @@
[supervisord] ## This is the main process for the Supervisor
nodaemon=true
[program:node]
command=node /app/apps/app/server.js
autostart=true
autorestart=true
stderr_logfile=/var/log/node.err.log
stdout_logfile=/var/log/node.out.log
[program:python]
directory=/code
command=sh bin/takeoff
autostart=true
autorestart=true
stderr_logfile=/var/log/python.err.log
stdout_logfile=/var/log/python.out.log
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
stderr_logfile=/var/log/nginx.err.log
stdout_logfile=/var/log/nginx.out.log

View File

@ -14,9 +14,9 @@
"clean": "turbo run clean"
},
"devDependencies": {
"config": "workspace:*",
"eslint-config-custom": "*",
"prettier": "latest",
"turbo": "latest"
},
"packageManager": "pnpm@7.24.3"
"packageManager": "yarn@1.22.19"
}

View File

@ -1,45 +0,0 @@
module.exports = {
extends: ["next", "turbo", "prettier"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["react", "@typescript-eslint"],
settings: {
next: {
rootDir: ["apps/*/", "packages/*/"],
},
},
rules: {
"@next/next/no-html-link-for-pages": "off",
"prefer-const": "error",
"no-irregular-whitespace": "error",
"no-trailing-spaces": "error",
"no-duplicate-imports": "error",
"arrow-body-style": ["error", "as-needed"],
"react/self-closing-comp": ["error", { component: true, html: true }],
"import/order": [
"warn",
{
groups: ["external", "parent", "sibling", "index", "object", "type"],
pathGroups: [
{
pattern: "@/**/**",
group: "parent",
position: "before",
},
],
},
],
"no-restricted-imports": [
"error",
{
patterns: ["../"],
},
],
},
};

View File

@ -1,8 +0,0 @@
module.exports = {
extends: ["next", "prettier"],
settings: {
next: {
rootDir: ["apps/*/", "packages/*/"],
},
},
};

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,10 +0,0 @@
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};

View File

@ -0,0 +1,20 @@
module.exports = {
extends: ["next", "turbo", "prettier"],
parser: "@typescript-eslint/parser",
plugins: ["react", "@typescript-eslint"],
settings: {
next: {
rootDir: ["app/", "docs/", "packages/*/"],
},
},
rules: {
"@next/next/no-html-link-for-pages": "off",
"react/jsx-key": "off",
"prefer-const": "error",
"no-irregular-whitespace": "error",
"no-trailing-spaces": "error",
"no-duplicate-imports": "error",
"arrow-body-style": ["error", "as-needed"],
"react/self-closing-comp": ["error", { component: true, html: true }],
},
};

View File

@ -1,5 +1,5 @@
{
"name": "config",
"name": "eslint-config-custom",
"version": "0.0.0",
"main": "index.js",
"license": "MIT",

View File

@ -0,0 +1,3 @@
export const Button = () => {
return <button>button</button>;
};

View File

@ -14,4 +14,4 @@
// export * from "./spinner";
// export * from "./text-area";
// export * from "./tooltip";
export {};
export * from "./button";

View File

@ -10,12 +10,13 @@
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"classnames": "^2.3.2",
"config": "workspace:*",
"eslint": "^7.32.0",
"eslint-config-custom": "*",
"next": "12.3.2",
"react": "^18.2.0",
"tsconfig": "workspace:*",
"tsconfig": "*",
"typescript": "4.7.4"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
packages:
- 'apps/*'
- "packages/*"

4
setup.sh Normal file
View File

@ -0,0 +1,4 @@
# Generating API Server environmental variables
cp ./apiserver/.env.example ./apiserver/.env
# Generating App environmental variables
cp ./apps/app/.env.example ./apps/app/.env

View File

@ -9,7 +9,9 @@
"NEXT_PUBLIC_DOCSEARCH_INDEX_NAME",
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_AUTH_TOKEN",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT"
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
"NEXT_PUBLIC_ENABLE_SENTRY",
"NEXT_PUBLIC_ENABLE_OAUTH"
],
"pipeline": {
"build": {

9095
yarn.lock Normal file

File diff suppressed because it is too large Load Diff