Build + offer docker image and docker-compose.yml file for easier self-hosting

Tweak login and auth for IP-based setups and setups without email enabled.
This commit is contained in:
Bruno Bernardino
2024-04-09 13:22:05 +01:00
parent 5a85dd224e
commit 735b14544a
8 changed files with 119 additions and 13 deletions

View File

@@ -1,7 +1,7 @@
PORT=8000 PORT=8000
BASE_URL="http://localhost:8000" BASE_URL="http://localhost:8000"
POSTGRESQL_HOST="localhost" POSTGRESQL_HOST="postgresql" # docker container name or external hostname/IP
POSTGRESQL_USER="postgres" POSTGRESQL_USER="postgres"
POSTGRESQL_PASSWORD="fake" POSTGRESQL_PASSWORD="fake"
POSTGRESQL_DBNAME="bewcloud" POSTGRESQL_DBNAME="bewcloud"

View File

@@ -0,0 +1,47 @@
name: "Build Docker Image"
on:
workflow_dispatch:
push:
branches:
- "main"
release:
types: [published]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -2,6 +2,8 @@ FROM denoland/deno:ubuntu-1.41.3
EXPOSE 8000 EXPOSE 8000
RUN apt-get update && apt-get install -y make
WORKDIR /app WORKDIR /app
# These steps will be re-run upon each file change in your working directory: # These steps will be re-run upon each file change in your working directory:

View File

@@ -9,7 +9,14 @@ This is the [bewCloud app](https://bewcloud.com) built using [Fresh](https://fre
## Self-host it! ## Self-host it!
Check the [Development section below](#development). Download/copy [`docker-compose.yml`](/docker-compose.yml) and [`.env.sample`](/.env.sample) as `.env`.
```sh
$ docker compose up # makes the app available at http://localhost:8000
$ docker compose run website bash -c "cd /app && make migrate-db" # initializes/updates the database (only needs to be executed the first time and on any updates)
```
Alternatively, check the [Development section below](#development).
> [!IMPORTANT] > [!IMPORTANT]
> Even with signups disabled (`CONFIG_ALLOW_SIGNUPS="false"`), the first signup will work and become an admin. > Even with signups disabled (`CONFIG_ALLOW_SIGNUPS="false"`), the first signup will work and become an admin.
@@ -25,7 +32,7 @@ Don't forget to set up your `.env` file based on `.env.sample`.
## Development ## Development
```sh ```sh
$ docker compose up # (optional) runs docker with postgres, locally $ docker compose -f docker-compose.dev.yml up # (optional) runs docker with postgres, locally
$ make migrate-db # runs any missing database migrations $ make migrate-db # runs any missing database migrations
$ make start # runs the app $ make start # runs the app
$ make format # formats the code $ make format # formats the code

20
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
postgresql:
image: postgres:15
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=fake
- POSTGRES_DB=bewcloud
restart: on-failure
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- 5432:5432
ulimits:
memlock:
soft: -1
hard: -1
volumes:
pgdata:
driver: local

View File

@@ -1,4 +1,14 @@
services: services:
website:
build: ghcr.io/bewcloud/bewcloud
restart: always
ports:
- 127.0.0.1:8000:8000
mem_limit: '256m'
user: "${UID}:${GID}" # if you run into issues with permissions for the data-files volume below, see other options at https://stackoverflow.com/a/56904335
volumes:
- ./data-files:/app/data-files
postgresql: postgresql:
image: postgres:15 image: postgres:15
environment: environment:
@@ -7,14 +17,15 @@ services:
- POSTGRES_DB=bewcloud - POSTGRES_DB=bewcloud
restart: on-failure restart: on-failure
volumes: volumes:
- pgdata:/var/lib/postgresql/data - bewcloud-db:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 127.0.0.1:5432:5432
ulimits: ulimits:
memlock: memlock:
soft: -1 soft: -1
hard: -1 hard: -1
mem_limit: '256m'
volumes: volumes:
pgdata: bewcloud-db:
driver: local driver: local

View File

@@ -18,6 +18,8 @@ export interface JwtData {
}; };
} }
const isBaseUrlAnIp = () => /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(baseUrl);
const textToData = (text: string) => new TextEncoder().encode(text); const textToData = (text: string) => new TextEncoder().encode(text);
export const dataToText = (data: Uint8Array) => new TextDecoder().decode(data); export const dataToText = (data: Uint8Array) => new TextDecoder().decode(data);
@@ -152,15 +154,18 @@ export async function logoutUser(request: Request) {
name: COOKIE_NAME, name: COOKIE_NAME,
value: '', value: '',
expires: tomorrow, expires: tomorrow,
domain: isRunningLocally(request)
? 'localhost'
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0],
path: '/', path: '/',
secure: isRunningLocally(request) ? false : true, secure: isRunningLocally(request) ? false : true,
httpOnly: true, httpOnly: true,
sameSite: 'Lax', sameSite: 'Lax',
}; };
if (!isBaseUrlAnIp()) {
cookie.domain = isRunningLocally(request)
? 'localhost'
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
}
const response = new Response('Logged Out', { const response = new Response('Logged Out', {
status: 303, status: 303,
headers: { 'Location': '/', 'Content-Type': 'text/html; charset=utf-8' }, headers: { 'Location': '/', 'Content-Type': 'text/html; charset=utf-8' },
@@ -203,13 +208,18 @@ export async function createSessionCookie(
name: COOKIE_NAME, name: COOKIE_NAME,
value: token, value: token,
expires: newSession.expires_at, expires: newSession.expires_at,
domain: isRunningLocally(request) ? 'localhost' : baseUrl.replace('https://', ''),
path: '/', path: '/',
secure: isRunningLocally(request) ? false : true, secure: isRunningLocally(request) ? false : true,
httpOnly: true, httpOnly: true,
sameSite: 'Lax', sameSite: 'Lax',
}; };
if (!isBaseUrlAnIp()) {
cookie.domain = isRunningLocally(request)
? 'localhost'
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
}
setCookie(response.headers, cookie); setCookie(response.headers, cookie);
return response; return response;
@@ -227,13 +237,18 @@ export async function updateSessionCookie(
name: COOKIE_NAME, name: COOKIE_NAME,
value: token, value: token,
expires: userSession.expires_at, expires: userSession.expires_at,
domain: isRunningLocally(request) ? 'localhost' : baseUrl.replace('https://', ''),
path: '/', path: '/',
secure: isRunningLocally(request) ? false : true, secure: isRunningLocally(request) ? false : true,
httpOnly: true, httpOnly: true,
sameSite: 'Lax', sameSite: 'Lax',
}; };
if (!isBaseUrlAnIp()) {
cookie.domain = isRunningLocally(request)
? 'localhost'
: baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
}
setCookie(response.headers, cookie); setCookie(response.headers, cookie);
return response; return response;

View File

@@ -31,7 +31,11 @@ export const handler: Handlers<Data, FreshContextState> = {
email = searchParams.get('email') || ''; email = searchParams.get('email') || '';
formData.set('email', email); formData.set('email', email);
if (isEmailEnabled()) {
notice = `You have received a code in your email. Use it to verify your email and login.`; notice = `You have received a code in your email. Use it to verify your email and login.`;
} else {
notice = `Your account was created successfully. Login below.`;
}
} }
return await context.render({ notice, email, formData }); return await context.render({ notice, email, formData });
@@ -153,7 +157,7 @@ export default function Login({ data }: PageProps<Data, FreshContextState>) {
: null} : null}
<form method='POST' class='mb-12'> <form method='POST' class='mb-12'>
{formFields(data?.email, data?.notice?.includes('verify your email')).map((field) => {formFields(data?.email, data?.notice?.includes('verify your email') && isEmailEnabled()).map((field) =>
generateFieldHtml(field, data?.formData || new FormData()) generateFieldHtml(field, data?.formData || new FormData())
)} )}
<section class='flex justify-center mt-8 mb-4'> <section class='flex justify-center mt-8 mb-4'>