a unified docker image

This commit is contained in:
Eugene Pankov 2021-11-21 00:08:04 +01:00
parent e1b0d01ac3
commit 34ac0781c2
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
11 changed files with 174 additions and 34 deletions

49
.github/workflows/docker.yml vendored Normal file
View File

@ -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 }}

56
Dockerfile Normal file
View File

@ -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"]

View File

@ -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.) * A database server supported by Django (MariaDB, Postgres, SQLite, etc.)
* Storage for distribution files - local, S3, GCS or others supported by `fsspec` * 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 ## Environment variables
### Frontend
* `BACKEND_URL` (required if running the backend in a separate Docker container).
* `WEB_CONCURRENCY`
### Backend
* `DATABASE_URL` (required). * `DATABASE_URL` (required).
* `FRONTEND_URL`
* `APP_DIST_STORAGE`: a `file://`, `s3://`, or `gcs://` URL to store app distros in. * `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`. * `SOCIAL_AUTH_*_KEY` & `SOCIAL_AUTH_*_SECRET`: social login credentials, supported providers are `GITHUB`, `GITLAB`, `MICROSOFT_GRAPH` and `GOOGLE_OAUTH2`.
## Adding Tabby app versions ## 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 # Development setup

View File

@ -1 +1,2 @@
__pycache__ __pycache__
public

4
backend/manage.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
/wait
cd /app
/venv/*/bin/python ./manage.py $@

View File

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
/wait
cd /app cd /app
./manage.py migrate /venv/*/bin/python ./manage.py migrate
gunicorn /venv/*/bin/gunicorn

View File

@ -42,7 +42,7 @@ class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet):
gateways = list(self.queryset) gateways = list(self.queryset)
random.shuffle(gateways) random.shuffle(gateways)
if not len(gateways): if not len(gateways):
raise NotFound() raise NoGatewaysError()
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
try: try:

View File

@ -12,12 +12,12 @@ class IndexView(APIView):
def get(self, request, format=None): def get(self, request, format=None):
if settings.FRONTEND_URL: if settings.FRONTEND_URL:
return HttpResponseRedirect(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): class TerminalView(APIView):
def get(self, request, format=None): 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' response['X-Frame-Options'] = 'SAMEORIGIN'
return response return response

View File

@ -9,8 +9,6 @@ load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
FRONTEND_BUILD_DIR = BASE_DIR / '../frontend/build'
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure') SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure')
DEBUG = bool(os.getenv('DEBUG', False)) 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' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CSRF_USE_SESSIONS = False 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')) 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('/') 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 FRONTEND_URL = None
BACKEND_URL = None BACKEND_URL = None
@ -229,6 +223,12 @@ else:
GITHUB_ELIGIBLE_SPONSORSHIPS = [] GITHUB_ELIGIBLE_SPONSORSHIPS = []
STATIC_URL = '/static/'
if FRONTEND_BUILD_DIR.exists():
STATICFILES_DIRS = [FRONTEND_BUILD_DIR]
STATIC_ROOT = BASE_DIR / 'public'
if FRONTEND_URL: if FRONTEND_URL:
CORS_ALLOWED_ORIGINS = [FRONTEND_URL] CORS_ALLOWED_ORIGINS = [FRONTEND_URL]
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True

21
docker-compose.split.yml Normal file
View File

@ -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

View File

@ -1,21 +1,26 @@
services: services:
frontend: tabby:
build: frontend build: .
restart: always
depends_on:
- db
ports: ports:
- 9090:80 - 9090:80
environment:
- PORT=80
- BACKEND_URL=http://localhost:9091
backend:
build: backend
ports:
- 9091:80
volumes: volumes:
- ./app-dist:/app-dist - ./app-dist:/app-dist
environment: environment:
- DATABASE_URL - DATABASE_URL=mysql://root:123@db/tabby
- PORT=80 - PORT=80
- FRONTEND_URL=http://localhost:9090
- ENABLE_HOMEPAGE=False - ENABLE_HOMEPAGE=False
- DEBUG=False - DEBUG=False
- APP_DIST_STORAGE=file:///app-dist - 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