From 34ac0781c2b0fb02970b69c0e0cde721cfcd5cb6 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sun, 21 Nov 2021 00:08:04 +0100 Subject: [PATCH] a unified docker image --- .github/workflows/docker.yml | 49 ++++++++++++++++++++++++++++ Dockerfile | 56 ++++++++++++++++++++++++++++++++ README.md | 25 +++++++------- backend/.dockerignore | 1 + backend/manage.sh | 4 +++ backend/start.sh | 5 +-- backend/tabby/app/api/gateway.py | 2 +- backend/tabby/app/views.py | 4 +-- backend/tabby/settings.py | 14 ++++---- docker-compose.split.yml | 21 ++++++++++++ docker-compose.yml | 27 ++++++++------- 11 files changed, 174 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 backend/manage.sh create mode 100644 docker-compose.split.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..627135c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,49 @@ +name: Docker + +on: + schedule: + - cron: '25 12 * * *' + push: + branches: [ master ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ master ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: eugeny/tabby-web + + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + build-args: EXTRA_DEPS=gcsfs + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f2ee815 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1 +FROM node:12-alpine AS frontend-build +WORKDIR /app +COPY frontend/package.json frontend/yarn.lock ./ +RUN yarn +COPY frontend/webpack* frontend/tsconfig.json ./ +COPY frontend/assets assets +COPY frontend/src src +COPY frontend/theme theme +RUN yarn run build +RUN yarn run build:server + +FROM node:12-alpine AS frontend +WORKDIR /app +COPY --from=frontend-build /app/build build +COPY --from=frontend-build /app/build-server build-server +COPY frontend/package.json . + +CMD ["npm", "start"] + +# ---- + +FROM python:3.7-alpine AS build-backend +ARG EXTRA_DEPS + +RUN apk add build-base musl-dev libffi-dev openssl-dev mariadb-dev + +WORKDIR /app +RUN pip install -U setuptools 'cryptography>=3.0,<3.1' poetry==1.1.7 +COPY backend/pyproject.toml backend/poetry.lock ./ +RUN poetry config virtualenvs.path /venv +RUN poetry install --no-dev --no-ansi --no-interaction +RUN poetry run pip install -U setuptools $EXTRA_DEPS + +COPY backend/manage.py backend/gunicorn.conf.py ./ +COPY backend/tabby tabby +COPY --from=frontend /app/build /frontend + +ARG BUNDLED_TABBY=1.0.163 + +RUN FRONTEND_BUILD_DIR=/frontend /venv/*/bin/python ./manage.py collectstatic --noinput +RUN FRONTEND_BUILD_DIR=/frontend /venv/*/bin/python ./manage.py add_version ${BUNDLED_TABBY} + +FROM python:3.7-alpine AS backend + +ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait /wait +RUN chmod +x /wait + +RUN apk add mariadb-connector-c + +COPY --from=build-backend /app /app +COPY --from=build-backend /venv /venv + +COPY backend/start.sh backend/manage.sh / +RUN chmod +x /start.sh /manage.sh +CMD ["/start.sh"] diff --git a/README.md b/README.md index 15b942a..6a935ad 100644 --- a/README.md +++ b/README.md @@ -16,27 +16,30 @@ Tabby Web serves the [Tabby Terminal](https://github.com/Eugeny/tabby) as a web * A database server supported by Django (MariaDB, Postgres, SQLite, etc.) * Storage for distribution files - local, S3, GCS or others supported by `fsspec` -# Using Docker images +# Quickstart (using `docker-compose`) -Tabby Web consists of two Docker images - `backend` and `frontend`. See an example set up in `docker-compose.yml` +You'll need: + +* OAuth credentials from GitHub, GitLab, Google or Microsoft for authentication. +* For SSH and Telnet: a [`tabby-connection-gateway`](https://github.com/Eugeny/tabby-connection-gateway) to forward traffic. + +```bash + docker-compose up -e SOCIAL_AUTH_GITHUB_KEY=xxx -e SOCIAL_AUTH_GITHUB_SECRET=yyy +``` + +will start Tabby Web on port 9090 with MariaDB as a storage backend. + +For SSH and Telnet, once logged in, enter your connection gateway address and auth token in the settings. ## Environment variables -### Frontend - -* `BACKEND_URL` (required if running the backend in a separate Docker container). -* `WEB_CONCURRENCY` - -### Backend - * `DATABASE_URL` (required). -* `FRONTEND_URL` * `APP_DIST_STORAGE`: a `file://`, `s3://`, or `gcs://` URL to store app distros in. * `SOCIAL_AUTH_*_KEY` & `SOCIAL_AUTH_*_SECRET`: social login credentials, supported providers are `GITHUB`, `GITLAB`, `MICROSOFT_GRAPH` and `GOOGLE_OAUTH2`. ## Adding Tabby app versions -* `docker-compose run backend ./manage.py add_version 1.0.156-nightly.2` +* `docker-compose run tabby /manage.sh add_version 1.0.163` # Development setup diff --git a/backend/.dockerignore b/backend/.dockerignore index bee8a64..8aca0e0 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1 +1,2 @@ __pycache__ +public diff --git a/backend/manage.sh b/backend/manage.sh new file mode 100644 index 0000000..bf4f45d --- /dev/null +++ b/backend/manage.sh @@ -0,0 +1,4 @@ +#!/bin/sh +/wait +cd /app +/venv/*/bin/python ./manage.py $@ diff --git a/backend/start.sh b/backend/start.sh index 7cd1574..7e7f498 100755 --- a/backend/start.sh +++ b/backend/start.sh @@ -1,4 +1,5 @@ #!/bin/sh +/wait cd /app -./manage.py migrate -gunicorn +/venv/*/bin/python ./manage.py migrate +/venv/*/bin/gunicorn diff --git a/backend/tabby/app/api/gateway.py b/backend/tabby/app/api/gateway.py index df203d0..89faf32 100644 --- a/backend/tabby/app/api/gateway.py +++ b/backend/tabby/app/api/gateway.py @@ -42,7 +42,7 @@ class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet): gateways = list(self.queryset) random.shuffle(gateways) if not len(gateways): - raise NotFound() + raise NoGatewaysError() loop = asyncio.new_event_loop() try: diff --git a/backend/tabby/app/views.py b/backend/tabby/app/views.py index ca95250..bd32f15 100644 --- a/backend/tabby/app/views.py +++ b/backend/tabby/app/views.py @@ -12,12 +12,12 @@ class IndexView(APIView): def get(self, request, format=None): if settings.FRONTEND_URL: return HttpResponseRedirect(settings.FRONTEND_URL) - return static.serve(request, 'index.html', document_root=str(settings.FRONTEND_BUILD_DIR)) + return static.serve(request, 'index.html', document_root=str(settings.STATIC_ROOT)) class TerminalView(APIView): def get(self, request, format=None): - response = static.serve(request, 'terminal.html', document_root=str(settings.FRONTEND_BUILD_DIR)) + response = static.serve(request, 'terminal.html', document_root=str(settings.STATIC_ROOT)) response['X-Frame-Options'] = 'SAMEORIGIN' return response diff --git a/backend/tabby/settings.py b/backend/tabby/settings.py index 74d3325..7b2aef3 100644 --- a/backend/tabby/settings.py +++ b/backend/tabby/settings.py @@ -9,8 +9,6 @@ load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -FRONTEND_BUILD_DIR = BASE_DIR / '../frontend/build' - SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure') DEBUG = bool(os.getenv('DEBUG', False)) @@ -135,11 +133,6 @@ LOGGING = { }, } -STATIC_URL = '/static/' -if FRONTEND_BUILD_DIR.exists(): - STATICFILES_DIRS = [FRONTEND_BUILD_DIR] -STATIC_ROOT = BASE_DIR / 'public' - DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' CSRF_USE_SESSIONS = False @@ -172,6 +165,7 @@ SOCIAL_AUTH_PIPELINE = ( APP_DIST_STORAGE = os.getenv('APP_DIST_STORAGE', 'file://' + str(BASE_DIR / 'app-dist')) NPM_REGISTRY = os.getenv('NPM_REGISTRY', 'https://registry.npmjs.org').rstrip('/') +FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / '../frontend/build')) FRONTEND_URL = None BACKEND_URL = None @@ -229,6 +223,12 @@ else: GITHUB_ELIGIBLE_SPONSORSHIPS = [] +STATIC_URL = '/static/' +if FRONTEND_BUILD_DIR.exists(): + STATICFILES_DIRS = [FRONTEND_BUILD_DIR] +STATIC_ROOT = BASE_DIR / 'public' + + if FRONTEND_URL: CORS_ALLOWED_ORIGINS = [FRONTEND_URL] CORS_ALLOW_CREDENTIALS = True diff --git a/docker-compose.split.yml b/docker-compose.split.yml new file mode 100644 index 0000000..d258cb2 --- /dev/null +++ b/docker-compose.split.yml @@ -0,0 +1,21 @@ +services: + frontend: + build: frontend + ports: + - 9090:80 + environment: + - PORT=80 + - BACKEND_URL=http://localhost:9091 + backend: + build: backend + ports: + - 9091:80 + volumes: + - ./app-dist:/app-dist + environment: + - DATABASE_URL + - PORT=80 + - FRONTEND_URL=http://localhost:9090 + - ENABLE_HOMEPAGE=False + - DEBUG=False + - APP_DIST_STORAGE=file:///app-dist diff --git a/docker-compose.yml b/docker-compose.yml index d258cb2..8ba94c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,26 @@ services: - frontend: - build: frontend + tabby: + build: . + restart: always + depends_on: + - db ports: - 9090:80 - environment: - - PORT=80 - - BACKEND_URL=http://localhost:9091 - backend: - build: backend - ports: - - 9091:80 volumes: - ./app-dist:/app-dist environment: - - DATABASE_URL + - DATABASE_URL=mysql://root:123@db/tabby - PORT=80 - - FRONTEND_URL=http://localhost:9090 - ENABLE_HOMEPAGE=False - DEBUG=False - APP_DIST_STORAGE=file:///app-dist + - WAIT_HOSTS=db:3306 + + db: + image: mariadb:10.7.1 + restart: always + environment: + MARIADB_DATABASE: tabby + MARIADB_USER: user + MARIADB_PASSWORD: 123 + MYSQL_ROOT_PASSWORD: 123