diff --git a/.dockerignore b/.dockerignore index 56e5081d..251c890e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,5 +3,4 @@ **/.parcel-cache **/dist **/dist-server -**/.env **/*.log \ No newline at end of file diff --git a/.etc-hosts-sample b/.etc-hosts-sample deleted file mode 100644 index ca95e515..00000000 --- a/.etc-hosts-sample +++ /dev/null @@ -1,3 +0,0 @@ -127.0.0.1 pockethost.local # The main domain -127.0.0.1 pockethost-central.pockethost.local # The main pocketbase instance -127.0.0.1 test.pockethost.local # A sample (user) pocketbase instance \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3d9fe374..087f9f85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -.DS_Store -node_modules +**/.DS_Store +**/node_modules .secret .vscode -*.out -.env -*.log +**/*.out +**/*.log .idea +**/.env +**/.env.* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2b4752df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:18-alpine as buildbox +COPY --from=golang:1.19-alpine /usr/local/go/ /usr/local/go/ +ENV PATH="/usr/local/go/bin:${PATH}" +RUN apk add python3 py3-pip make gcc musl-dev g++ bash +# WORKDIR /src + +# COPY packages/pocketbase/src packages/pocketbase/src +# WORKDIR /src/packages/pocketbase/src +# RUN go get + +# WORKDIR /src +# COPY packages/common/package.json packages/common/ +# COPY packages/daemon/package.json packages/daemon/ +# COPY packages/daemon/yarn.lock packages/daemon/ +# COPY packages/pockethost.io/package.json packages/pockethost.io/ +# COPY packages/pockethost.io/yarn.lock packages/pockethost.io/ +# COPY package.json ./ +# COPY yarn.lock ./ +# RUN yarn +# COPY . . +# RUN yarn build +# RUN ls -lah node_modules diff --git a/fly.io/Dockerfile b/attic/fly.io/Dockerfile similarity index 100% rename from fly.io/Dockerfile rename to attic/fly.io/Dockerfile diff --git a/fly.io/fly.toml b/attic/fly.io/fly.toml similarity index 100% rename from fly.io/fly.toml rename to attic/fly.io/fly.toml diff --git a/fly.io/pocketbase-custom/go.mod b/attic/fly.io/pocketbase-custom/go.mod similarity index 100% rename from fly.io/pocketbase-custom/go.mod rename to attic/fly.io/pocketbase-custom/go.mod diff --git a/fly.io/pocketbase-custom/go.sum b/attic/fly.io/pocketbase-custom/go.sum similarity index 100% rename from fly.io/pocketbase-custom/go.sum rename to attic/fly.io/pocketbase-custom/go.sum diff --git a/fly.io/pocketbase-custom/pocketbase.go b/attic/fly.io/pocketbase-custom/pocketbase.go similarity index 100% rename from fly.io/pocketbase-custom/pocketbase.go rename to attic/fly.io/pocketbase-custom/pocketbase.go diff --git a/packages/daemon/samples/add.sh b/attic/samples/add.sh similarity index 100% rename from packages/daemon/samples/add.sh rename to attic/samples/add.sh diff --git a/packages/daemon/samples/certbot b/attic/samples/certbot similarity index 100% rename from packages/daemon/samples/certbot rename to attic/samples/certbot diff --git a/packages/daemon/samples/launch b/attic/samples/launch similarity index 100% rename from packages/daemon/samples/launch rename to attic/samples/launch diff --git a/packages/daemon/samples/nginx-template.conf b/attic/samples/nginx-template.conf similarity index 100% rename from packages/daemon/samples/nginx-template.conf rename to attic/samples/nginx-template.conf diff --git a/development.md b/development.md deleted file mode 100644 index 07cab492..00000000 --- a/development.md +++ /dev/null @@ -1,94 +0,0 @@ -# Developing just the frontend (Svelte) - -This is the easiest setup. - -```bash -git clone git@github.com:benallfree/pockethost.git -cd pockethost/packages/pockethost.io -cp .env-template-frontend-only .env -yarn dev -``` - -That's it. You're in business. Your local Svelte build will talk to the pockethost.io mothership and connect to that for all database-related tasks. - -# Developing the backend using `docker-compose` - -> WARNING: here there be monsters! Now entering Docker territory - tread softly and at your own peril - -The entire pockethost.io stack is dockerized to make it as easy as possible to reproduce the production environment. It is a somewhat tedious development cycle because every change requires a build and a container to be restarted. - -**Clone the repo** - -```bash -git clone git@github.com:benallfree/pockethost.git -cd pockethost -``` - -**Edit `/etc/hosts`** - -You need at least 3 host entries. One for the main domain, one for the database that tracks everything (the main pockethost.io db), and one (or more) for any instances you want to create a test. Wildcarding is not supported in `/etc/hosts`, so you have to make a manual entry for any PB instance you want to create and test. See `.etc-hosts-sample` for details. - -``` -127.0.0.1 pockethost.local # The main domain -127.0.0.1 pockethost-central.pockethost.local # The main pocketbase instance -127.0.0.1 test.pockethost.local # A sample (user) pocketbase instance -``` - -**Build custom PockeBase** - -_Any time you change the PocketBase code, you need to rebuild (`yarn build:_`) and restart `docker-compose`\_ - -This is to build the binary that runs INSIDE Docker. The Docker container will run using the same architecture as the host machine. If you are running an x86 machine, you'll probably need `build:386`. If you're running on Linux or Mac, then `arm64` is the one you want. You can try them both if you aren't sure. The worst that will happen is the `pocketbase` binary won't execute in Docker and you'll quickly discover that. - -```bash -cd packages/pocketbase -go install -yarn build:arm64 -yarn build:386 -``` - -**Build daemon** - -_Any time you change the daemon code, you need to rebuild (`yarn build`) and restart `docker-compose`_ - -```bash -cd packages/daemon -yarn build -``` - -**Build web app** - -_Any time you change the web app, you need to rebuild (`yarn build`) and restart `docker-compose`_ - -```bash -cd packages/pockethost.io -cp .env-template .env -yarn build -``` - -**Prepare Docker environment vars** - -```bash -cd docker -cp .env.template .env -``` - -Edit `APP_DOMAIN` to match `/etc/hosts` and `CORE_PB_PASSWORD` (needed by daemon) - -**Run** - -Finally, you can try running! - -Note, the first time you run, if it's a fresh database, it won't be able to log in. You should see a note in the Docker log if that happens. - -```bash -sudo docker-compose up -``` - -# Production Deployment - -Coming soon. - -```bash -sudo certbot --server https://acme-v02.api.letsencrypt.org/directory -d *.pockethost.io -d pockethost.io --manual --preferred-challenges dns-01 certonly -``` diff --git a/docker/.env-template b/docker/.env-template deleted file mode 100644 index e7688d63..00000000 --- a/docker/.env-template +++ /dev/null @@ -1,6 +0,0 @@ -APP_DOMAIN = pockethost.local -CORE_PB_SUBDOMAIN = pockethost-central -CORE_PB_USERNAME = ben@pockethost.io -CORE_PB_PASSWORD = -CORE_PB_PORT = 8090 -PB_IDLE_TTL = 5000 diff --git a/docker/.env-template-dev b/docker/.env-template-dev new file mode 100644 index 00000000..26063a4c --- /dev/null +++ b/docker/.env-template-dev @@ -0,0 +1,12 @@ +PUBLIC_APP_DOMAIN=pockethost.test +PUBLIC_PB_DOMAIN=pockethost.test +PUBLIC_PB_SUBDOMAIN=pockethost-central +DAEMON_PB_BIN_DIR=/src/packages/pocketbase/dist +DAEMON_PB_DATA_DIR=/data +DAEMON_PB_USERNAME=#ADDME +DAEMON_PB_PASSWORD=#FIXME +DAEMON_PB_PORT=8090 +DAEMON_IDLE_TTL=5000 +GOPATH=/go +YARN_CACHE=/usr/local/share/.cache/yarn/v6 +SHELL=/bin/bash \ No newline at end of file diff --git a/docker/.env-template-prod b/docker/.env-template-prod new file mode 100644 index 00000000..c3846b46 --- /dev/null +++ b/docker/.env-template-prod @@ -0,0 +1,13 @@ +PUBLIC_APP_DOMAIN=pockethost.io +PUBLIC_PB_DOMAIN=pockethost.io +PUBLIC_PB_SUBDOMAIN=pockethost-central +DAEMON_PB_BIN_DIR=/src/packages/pocketbase/dist +DAEMON_PB_DATA_DIR=/data +DAEMON_PB_USERNAME=#ADDME +DAEMON_PB_PASSWORD=#FIXME +DAEMON_PB_PORT=8090 +# 30 minutes +DAEMON_PB_IDLE_TTL=1800000 +GOPATH=/go +YARN_CACHE=/usr/local/share/.cache/yarn/v6 +SHELL=/bin/bash \ No newline at end of file diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml new file mode 100644 index 00000000..a4e8d024 --- /dev/null +++ b/docker/docker-compose-dev.yaml @@ -0,0 +1,80 @@ +version: '3' + +services: + prepbox: + env_file: + - .env.local + build: + context: .. + dockerfile: Dockerfile + container_name: prepbox + working_dir: /src + command: bash -c "yarn" + volumes: + - ./mount/cache/go:/go + - ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6 + - ..:/src + www: + env_file: + - .env.local + build: + context: .. + dockerfile: Dockerfile + container_name: www + restart: unless-stopped + working_dir: /src + command: bash -c "yarn dev:www --host=www" + volumes: + - ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6 + - ..:/src + networks: + - app-network + ports: + - '9000:5173' + depends_on: + daemon: + condition: service_started + prepbox: + condition: service_completed_successfully + daemon: + env_file: + - .env.local + build: + context: .. + dockerfile: Dockerfile + container_name: daemon + working_dir: /src + command: bash -c "yarn dev:daemon" + restart: unless-stopped + volumes: + - ./mount/daemon/instances:/data + - ./mount/cache/go:/go + - ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6 + - ..:/src + networks: + - app-network + ports: + - '9001:3000' + depends_on: + prepbox: + condition: service_completed_successfully + nginx: + image: nginx:mainline-alpine + container_name: nginx + restart: unless-stopped + depends_on: + - www + - daemon + ports: + - '80:80' + - '443:443' + volumes: + - ./mount/nginx/conf.d/local:/etc/nginx/conf.d + - ./mount/nginx/logs:/mount/nginx/logs + - ./mount/nginx/ssl:/mount/nginx/ssl + networks: + - app-network + +networks: + app-network: + driver: bridge diff --git a/docker/docker-compose-prod.yaml b/docker/docker-compose-prod.yaml new file mode 100644 index 00000000..91007664 --- /dev/null +++ b/docker/docker-compose-prod.yaml @@ -0,0 +1,102 @@ +version: '3' + +services: + prepbox: + environment: + - GOPATH=/go + env_file: + - .env.local + build: + context: .. + dockerfile: Dockerfile + container_name: prepbox + working_dir: /src + command: bash -c "yarn" + volumes: + - ./mount/cache/go:/go + - ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6 + - ..:/src + profiles: ['build'] + buildbox: + environment: + - GOPATH=/go + env_file: + - .env.local + build: + context: .. + dockerfile: Dockerfile + container_name: buildbox + working_dir: /src + command: bash -c "yarn build" + volumes: + - ./mount/cache/go:/go + - ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6 + - ..:/src + depends_on: + prepbox: + condition: service_completed_successfully + profiles: ['build'] + www: + env_file: + - .env.local + build: + context: .. + dockerfile: Dockerfile + container_name: www + restart: unless-stopped + working_dir: /src + command: bash -c "yarn start:www --host=www" + volumes: + - ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6 + - ..:/src + networks: + - app-network + ports: + - '9000:5173' + depends_on: + daemon: + condition: service_started + profiles: ['serve'] + daemon: + env_file: + - .env.local + build: + context: .. + dockerfile: Dockerfile + container_name: daemon + working_dir: /src + command: bash -c "yarn start:daemon" + restart: unless-stopped + environment: + - SHELL=/bin/bash + volumes: + - /home/pockethost/data:/data + - ./mount/cache/go:/go + - ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6 + - ..:/src + networks: + - app-network + ports: + - '9001:3000' + profiles: ['serve'] + nginx: + image: nginx:mainline-alpine + container_name: nginx + restart: unless-stopped + depends_on: + - www + - daemon + ports: + - '80:80' + - '443:443' + volumes: + - ./mount/nginx/conf.d/prod:/etc/nginx/conf.d + - ./mount/nginx/logs:/mount/nginx/logs + - ./mount/nginx/ssl:/mount/nginx/ssl + networks: + - app-network + profiles: ['serve'] + +networks: + app-network: + driver: bridge diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml deleted file mode 100644 index 8283ff9a..00000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,55 +0,0 @@ -version: '3' - -services: - app: - image: node:18 - container_name: www - restart: unless-stopped - command: node index.js - working_dir: /mount/repo/packages/pockethost.io/dist-server - volumes: - - ..:/mount/repo - networks: - - app-network - ports: - - '9000:3000' - depends_on: - - pbproxy - env_file: - - ./.env - pbproxy: - env_file: - - ./.env - image: node:18 - container_name: pbproxy - restart: unless-stopped - command: yarn serve - working_dir: /mount/repo/packages/daemon/ - volumes: - - ..:/mount/repo - - ../packages/pocketbase/pocketbase:/mount/pocketbase/bin/pocketbase - - ./mount/pocketbase/instances:/mount/pocketbase/instances - networks: - - app-network - ports: - - '9001:3000' - nginx: - image: nginx:mainline-alpine - container_name: nginx - restart: unless-stopped - depends_on: - - app - - pbproxy - ports: - - '80:80' - - '443:443' - volumes: - - ./nginx-conf:/etc/nginx/conf.d - - ./mount/logs:/mount/logs - - ./mount/ssl:/mount/ssl - networks: - - app-network - -networks: - app-network: - driver: bridge diff --git a/docker/mount/.gitignore b/docker/mount/.gitignore index 63686889..0e10515a 100644 --- a/docker/mount/.gitignore +++ b/docker/mount/.gitignore @@ -1,2 +1,2 @@ -pocketbase +daemon logs/* \ No newline at end of file diff --git a/docker/mount/cache/.gitignore b/docker/mount/cache/.gitignore new file mode 100644 index 00000000..7736a633 --- /dev/null +++ b/docker/mount/cache/.gitignore @@ -0,0 +1,2 @@ +yarn +go \ No newline at end of file diff --git a/docker/mount/nginx/conf.d/local/nginx.conf b/docker/mount/nginx/conf.d/local/nginx.conf new file mode 100644 index 00000000..b26b2fb7 --- /dev/null +++ b/docker/mount/nginx/conf.d/local/nginx.conf @@ -0,0 +1,64 @@ + +server { + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + server_name www.pockethost.test; + return 301 $scheme://pockethost.test$request_uri; +} + +server { + listen 443 ssl; + server_name pockethost.test; + ssl_certificate /mount/nginx/ssl/pockethost.test.crt; + ssl_certificate_key /mount/nginx/ssl/pockethost.test.key; + access_log /mount/nginx/logs/access.log; + error_log /mount/nginx/logs/error.log; + + location / { + proxy_read_timeout 180s; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://www:5173; + } +} + +server { + listen 443 ssl; + + server_name *.pockethost.test; + ssl_certificate /mount/nginx/ssl/pockethost.test.crt; + ssl_certificate_key /mount/nginx/ssl/pockethost.test.key; + + access_log /mount/nginx/logs/access.log; + error_log /mount/nginx/logs/error.log; + + location / { + proxy_read_timeout 180s; + + # WebSocket support + proxy_buffering off; # For realtime + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://daemon:3000; + } +} diff --git a/docker/mount/nginx/conf.d/prod/nginx.conf b/docker/mount/nginx/conf.d/prod/nginx.conf new file mode 100644 index 00000000..99c216df --- /dev/null +++ b/docker/mount/nginx/conf.d/prod/nginx.conf @@ -0,0 +1,66 @@ + +server { + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + server_name www.pockethost.io; + return 301 $scheme://pockethost.io$request_uri; +} + +server { + listen 443 ssl; + + server_name pockethost.io; + ssl_certificate /mount/nginx/ssl/fullchain.pem; + ssl_certificate_key /mount/nginx/ssl/privkey.pem; + + access_log /mount/nginx/logs/access.log; + error_log /mount/nginx/logs/error.log; + + location / { + proxy_read_timeout 180s; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://www:3000; + } +} + +server { + listen 443 ssl; + + server_name *.pockethost.io; + ssl_certificate /mount/nginx/ssl/fullchain.pem; + ssl_certificate_key /mount/nginx/ssl/privkey.pem; + + access_log /mount/nginx/logs/access.log; + error_log /mount/nginx/logs/error.log; + + location / { + proxy_read_timeout 180s; + + # WebSocket support + proxy_buffering off; # For realtime + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://daemon:3000; + } +} \ No newline at end of file diff --git a/docker/mount/nginx/logs/.gitignore b/docker/mount/nginx/logs/.gitignore new file mode 100644 index 00000000..bf0824e5 --- /dev/null +++ b/docker/mount/nginx/logs/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/docker/mount/nginx/ssl/.gitignore b/docker/mount/nginx/ssl/.gitignore index 2b730189..07a05bf4 100644 --- a/docker/mount/nginx/ssl/.gitignore +++ b/docker/mount/nginx/ssl/.gitignore @@ -1,3 +1,4 @@ +*.pem *.crt *.key *.srl \ No newline at end of file diff --git a/docker/mount/nginx/ssl/create-ca.sh b/docker/mount/nginx/ssl/create-ca.sh new file mode 100755 index 00000000..50092d2d --- /dev/null +++ b/docker/mount/nginx/ssl/create-ca.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Generates your own Certificate Authority for development. +# This script should be executed just once. + +set -e + +if [ -f "ca.crt" ] || [ -f "ca.key" ]; then + echo -e "\e[41mCertificate Authority files already exist!\e[49m" + echo + echo -e "You only need a single CA even if you need to create multiple certificates." + echo -e "This way, you only ever have to import the certificate in your browser once." + echo + echo -e "If you want to restart from scratch, delete the \e[93mca.crt\e[39m and \e[93mca.key\e[39m files." + exit +fi + +# Generate private key +openssl genrsa -out ca.key 2048 + +# Generate root certificate +openssl req -x509 -new -nodes -subj "/C=US/O=_Development CA/CN=Development certificates" -key ca.key -sha256 -days 3650 -out ca.crt + +echo -e "\e[42mSuccess!\e[49m" +echo +echo "The following files have been written:" +echo -e " - \e[93mca.crt\e[39m is the public certificate that should be imported in your browser" +echo -e " - \e[93mca.key\e[39m is the private key that will be used by \e[93mcreate-certificate.sh\e[39m" +echo +echo "Next steps:" +echo -e " - Import \e[93mca.crt\e[39m in your browser" +echo -e " - run \e[93mcreate-certificate.sh example.com\e[39m" diff --git a/docker/mount/nginx/ssl/create-certificate.sh b/docker/mount/nginx/ssl/create-certificate.sh new file mode 100755 index 00000000..42de366d --- /dev/null +++ b/docker/mount/nginx/ssl/create-certificate.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# Generates a wildcard certificate for a given domain name. + +set -e + +if [ -z "$1" ]; then + echo -e "\e[43mMissing domain name!\e[49m" + echo + echo "Usage: $0 example.com" + echo + echo "This will generate a wildcard certificate for the given domain name and its subdomains." + exit +fi + +DOMAIN=$1 + +if [ ! -f "ca.key" ]; then + echo -e "\e[41mCertificate Authority private key does not exist!\e[49m" + echo + echo -e "Please run \e[93mcreate-ca.sh\e[39m first." + exit +fi + +# Generate a private key +openssl genrsa -out "$DOMAIN.key" 2048 + +# Create a certificate signing request +openssl req -new -subj "/C=US/O=Local Development/CN=$DOMAIN" -key "$DOMAIN.key" -out "$DOMAIN.csr" + +# Create a config file for the extensions +>"$DOMAIN.ext" cat <<-EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth, clientAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = $DOMAIN +DNS.2 = *.$DOMAIN +EOF + +# Create the signed certificate +openssl x509 -req \ + -in "$DOMAIN.csr" \ + -extfile "$DOMAIN.ext" \ + -CA ca.crt \ + -CAkey ca.key \ + -CAcreateserial \ + -out "$DOMAIN.crt" \ + -days 365 \ + -sha256 + +rm "$DOMAIN.csr" +rm "$DOMAIN.ext" + +echo -e "\e[42mSuccess!\e[49m" +echo +echo -e "You can now use \e[93m$DOMAIN.key\e[39m and \e[93m$DOMAIN.crt\e[39m in your web server." +echo -e "Don't forget that \e[1myou must have imported \e[93mca.crt\e[39m in your browser\e[0m to make it accept the certificate." diff --git a/docker/mount/ssl/.gitignore b/docker/mount/ssl/.gitignore deleted file mode 100644 index 612424a3..00000000 --- a/docker/mount/ssl/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.pem \ No newline at end of file diff --git a/docker/mount/ssl/mk.sh b/docker/mount/ssl/mk.sh deleted file mode 100644 index 53517407..00000000 --- a/docker/mount/ssl/mk.sh +++ /dev/null @@ -1,4 +0,0 @@ -openssl req -x509 -out pockethost.local.crt -keyout pockethost.local.key \ - -newkey rsa:2048 -nodes -sha256 \ - -subj '/CN=pockethost.local' -extensions EXT -config <( \ - printf "[dn]\nCN=pockethost.local\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:pockethost.local\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") diff --git a/docker/mount/ssl/pockethost.local.crt b/docker/mount/ssl/pockethost.local.crt deleted file mode 100644 index 4309fa15..00000000 --- a/docker/mount/ssl/pockethost.local.crt +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC+jCCAeKgAwIBAgIJAN2tVmx5CDN7MA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV -BAMMEHBvY2tldGhvc3QubG9jYWwwHhcNMjIxMDEyMDQ0MzE2WhcNMjIxMTExMDQ0 -MzE2WjAbMRkwFwYDVQQDDBBwb2NrZXRob3N0LmxvY2FsMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAxwuaR/H2qArNSrgQEfZMg5peZYqGSzhov30QrS4N -0YA/iRd6h0DLW76fJ+kta0Zx7J34EnYOodEW5tQ3dCQrGmHHlTVsR4iIMuk1ClEC -0OmDhVJc/hFvGIdeJhF7lsJ+NmUBKgYB5phEn21EOrHhTBREdIMjU3mnXiBCnCRF -cNti9/t1hLx0yXohCq8HNjEJEMk7QZdiFISPLeuf8a6Tp3NtMLcdfCVB3V80FReP -Vhl/Q0dx4nWuqbU6poAbfVr2Ot/bbtd4ZqhjI89vBDZWKC8tDNfvuRM/eKYc0VT5 -lg4I0hR1yLz8I6LZYpo2L6FUKA42PPI1D4zJTMBqbOpUmQIDAQABo0EwPzAbBgNV -HREEFDASghBwb2NrZXRob3N0LmxvY2FsMAsGA1UdDwQEAwIHgDATBgNVHSUEDDAK -BggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAPEBYhHTVikaeXCmU9Mlm3fe4 -UaofK7gd+CV2HvclW3BiiEFhBsiuIw/33PRJG3caj2EYYdW5TytKJ1962dwIiod8 -ve2XnHiIzP5ECgBiiGSu1BQCK4Olg9u6JRXPDsa1/qpuodikXnxWCVHiCwUDuECt -rKk6BggvzPfRv6X+DZkoyc4sx2NJbUTYyLBZq+v9DSX7WGP/3ZbUTQ3UHVyFSM3d -vTgbLTgfjNuwFx9WFg7JMfp4+GoNX4pei2nOagfqP8BUODQEkBcwZV1UZEO4Gbi/ -tw6ze/jJqIb80lu2U7hNzzsCKhVnYs+EBNPo88Cn+bpHVmxlK6T1M/OhMUGt0g== ------END CERTIFICATE----- diff --git a/docker/mount/ssl/pockethost.local.key b/docker/mount/ssl/pockethost.local.key deleted file mode 100644 index 31de9193..00000000 --- a/docker/mount/ssl/pockethost.local.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHC5pH8faoCs1K -uBAR9kyDml5lioZLOGi/fRCtLg3RgD+JF3qHQMtbvp8n6S1rRnHsnfgSdg6h0Rbm -1Dd0JCsaYceVNWxHiIgy6TUKUQLQ6YOFUlz+EW8Yh14mEXuWwn42ZQEqBgHmmESf -bUQ6seFMFER0gyNTeadeIEKcJEVw22L3+3WEvHTJeiEKrwc2MQkQyTtBl2IUhI8t -65/xrpOnc20wtx18JUHdXzQVF49WGX9DR3Hida6ptTqmgBt9WvY639tu13hmqGMj -z28ENlYoLy0M1++5Ez94phzRVPmWDgjSFHXIvPwjotlimjYvoVQoDjY88jUPjMlM -wGps6lSZAgMBAAECggEAKYeoNx6rIkCuuMpSwAytxj+tNm6CuqsYX+vOUPPs+itS -Fl6JuDKyu3+4YXFrgph+KKqFGgT75JNlvd/FejwZqjWAmQc+gmZgVI8H/BEPD1vJ -j0WyFWi5z1pfMH4xVTFbeCn25je+qchXeRQpSj3XDjKkXdMGyeH2I9ODUmTXAEbH -zA/FuBg+M3leqjak1mcog/e/FIkpZwJq2rMmwehe03gFZdJQgExjhBOXT6haVXQY -0SsjQG70gli9tLwVL/JIvhE6W5oaGsT1SyI76GWwBIL2WHrT53VdgFhienaHIRlg -KwcpfF2pBkuTtQLLDgvUOKSHHO5Rc93SqycDzXN1iQKBgQDpx6scQOFa86aown9b -6QwF9KiQgG2i1bCyifRyGjSQuOpgN22Dgq1hNczh+84Souz7WsNgiHzU2Bu1NqXC -/UJH5PcZv2qiqIifqLnKWWyNpK9+JyuP89wmrGIjylbbzhukY4qyb7WnDyH+FDU4 -/dRhMrFAc7Et5oAVgaOep74kBwKBgQDZ9sWDUP/KMtfitCXzn0uZWU4ahIAXru+v -UCrS1aiAmDZvOl6QTXl0IpbwDHzcCMLh8TQeUJt8rD7Limd4lrjZw2TfjKtLv2op -jLEWof8h2YZxwz2vOEE53aQvyoC57Tse94Sy2Bb+vJM9xWrdzSLp9pWw+bdhDu2A -g+a1vqvaXwKBgBrA6ugU1LVf5NazS3ftN58G3LOMvv8/jTUhbIQSU6msP8Y7EaFX -NxhE2+mQs4iWdKBfRrSpaf/Bq4oVcurZqNgpb83WhhGPT/NVj82EZlPfYOYC/Y0/ -zxXt7F1ELqSA9dDeQ2UgO52esbkt/tlC0yc8ceR6WPBzzHyplVv2vl/JAoGBANQ0 -IbWoXXBBMdfAZhaa5uJEhPriN0dXhHkdRqP/ac7Q4mZF4J0DIJTFvEe3ELS0Pu/0 -gjZlagvmMji95eEMdKlmR0Yx0O+tSzFqjVqomxkci30khWCbFz28IMZ6k/rwERgk -COiJ41FczMld854/wpcgADrN1BBFlUsCn9If8XZnAoGAN8AwAT+6/EucEOlcgSO0 -eVxBmvnABj8fNGylTJxIpwlJabkOl324bKbrTiqvY5zTKFWr3AhPOmckXcWhLycy -4TCqlBIWl76lt37zgPwPGiuSfaubKbWsbWDjfk27qlt2MNsG7IHYOfldWJkxPiyK -i0hsIapEVqcWsDaXo/3i1mE= ------END PRIVATE KEY----- diff --git a/docker/nginx-conf/nginx.conf b/docker/nginx-conf/nginx.conf deleted file mode 100644 index b511e6d2..00000000 --- a/docker/nginx-conf/nginx.conf +++ /dev/null @@ -1,85 +0,0 @@ - -server { - listen 80 default_server; - server_name _; - return 301 https://$host$request_uri; -} - -server { - - # dev - server_name www.pockethost.local; - return 301 $scheme://pockethost.local$request_uri; - - # prod - # server_name www.pockethost.io; - # return 301 $scheme://pockethost.io$request_uri; -} - -server { - listen 443 ssl; - - # dev - server_name pockethost.local; - ssl_certificate /mount/ssl/pockethost.local.crt; - ssl_certificate_key /mount/ssl/pockethost.local.key; - - # prod - # server_name pockethost.io; - # ssl_certificate /mount/ssl/fullchain.pem; - # ssl_certificate_key /mount/ssl/privkey.pem; - - access_log /mount/logs/access.log; - error_log /mount/logs/error.log; - - location / { - proxy_read_timeout 180s; - - # WebSocket support - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_pass http://www:3000; - } -} - -server { - listen 443 ssl; - - # dev - server_name *.pockethost.local; - ssl_certificate /mount/ssl/pockethost.local.crt; - ssl_certificate_key /mount/ssl/pockethost.local.key; - - - # prod -# server_name *.pockethost.io; -# ssl_certificate /mount/ssl/fullchain.pem; -# ssl_certificate_key /mount/ssl/privkey.pem; - - access_log /mount/logs/access.log; - error_log /mount/logs/error.log; - - location / { - proxy_read_timeout 180s; - - # WebSocket support - proxy_buffering off; # For realtime - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_pass http://pbproxy:3000; - } -} \ No newline at end of file diff --git a/docs/dnsmasq.md b/docs/dnsmasq.md new file mode 100644 index 00000000..ba596918 --- /dev/null +++ b/docs/dnsmasq.md @@ -0,0 +1,69 @@ +# Never touch your local /etc/hosts file in OS X again + +> To setup your computer to work with \*.test domains, e.g. project.test, awesome.test and so on, without having to add to your hosts file each time. + +## Requirements + +- [Homebrew](https://brew.sh/) + +## Install + +``` +brew install dnsmasq +``` + +## Setup + +### Create config directory + +``` +mkdir -pv $(brew --prefix)/etc/ +``` + +### Setup \*.test + +``` +echo 'address=/.test/127.0.0.1' >> $(brew --prefix)/etc/dnsmasq.conf +``` + +### Change port to default DNS port + +``` +echo 'port=53' >> $(brew --prefix)/etc/dnsmasq.conf +``` + +## Autostart - now and after reboot + +``` +sudo brew services start dnsmasq +``` + +## Test + +``` +dig testing.testing.one.two.three.test @127.0.0.1 +``` + +## Add to resolvers + +### Create resolver directory + +``` +sudo mkdir -v /etc/resolver +``` + +### Add your nameserver to resolvers + +``` +sudo bash -c 'echo "nameserver 127.0.0.1" > /etc/resolver/test' +``` + +### Test + +``` +# Make sure you haven't broken your DNS. +ping -c 1 www.google.com +# Check that .dev names work +ping -c 1 this.is.a.test.test +ping -c 1 iam.the.walrus.test +``` diff --git a/docs/etc_hosts.md b/docs/etc_hosts.md new file mode 100644 index 00000000..9120f706 --- /dev/null +++ b/docs/etc_hosts.md @@ -0,0 +1,19 @@ +# Manually set up `/etc/hosts` + +If you can't use [dnsmasq](./dnsmasq.md) or equivalent, you can still configure hosts manually. The dev experience will be slightly more limited, but it will still work. + +**1. Add host entries to `/etc./hosts`** + +```bash +sudo nano /etc/hosts +``` + +Then, add these entires: + +``` +127.0.0.1 pockethost.test # The main domain +127.0.0.1 pockethost-central.pockethost.test # The main pocketbase instance +127.0.0.1 test.pockethost.test # A sample (user) pocketbase instance +``` + +Add as many `*.pockethost.test` subdomains as you want to test. Since `/etc/hosts` does not support wildcarding, this must be done manually. diff --git a/docs/local-domain-setup.md b/docs/local-domain-setup.md new file mode 100644 index 00000000..f09da423 --- /dev/null +++ b/docs/local-domain-setup.md @@ -0,0 +1,40 @@ +# Local Domain Setup Instructions + +This document covers how to set up your local development environment to recongize wildcard domains with SSL. Developing the full Dockerized stack for PocketHost requires these steps. + +**1. Generate a root certificate for self-signing.** + +```bash +cd docker/mount/nginx/ssl +./create-ca.sh +``` + +**2. Manually approve the self-signed certificate in your browser.** + +For FireFox OS X, do this: + +1. Open settings page. +2. Search for "cert" +3. Click `View Certificates` +4. Move to the `Authorities` tab +5. Import `ca.crt` + +For other browsers and operating systems, follow the instructions here: https://github.com/BenMorel/dev-certificates + +**3. Generate a wildcard domain cert for `pockethost.test`** + +``` +./create-certificate pockethost.test +``` + +**4. Configure your machine to recognize wildcard localhost domains.** + +If you are on OS X, follow the [dnsmasq instructions](./dnsmasq.md) to set up your local machine for the ultimate local domain wildcard dev experience. + +Remember to start `dnsmasq` every time: + +```bash +brew services restart dnsmasq +``` + +If you don't want to use `dnsmasq`, follow the [manual /etc/hosts setup instructions](./etc_hosts.md). diff --git a/package.json b/package.json index cd21d8d1..3543cafa 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "build": "concurrently 'yarn build:pocketbase' 'yarn build:daemon' 'yarn build:www'", "build:pocketbase": "cd packages/pocketbase && yarn build", "build:daemon": "cd packages/daemon && yarn build", - "build:www": "cd packages/pockethost.io && yarn build" + "build:www": "cd packages/pockethost.io && yarn build", + "dev:www": "cd packages/pockethost.io && yarn dev", + "dev:daemon": "cd packages/daemon && yarn dev", + "start": "concurrently 'yarn start:www' 'yarn start:daemon'", + "start:www": "cd packages/pockethost.io && yarn start", + "start:daemon": "cd packages/daemon && yarn start" }, "workspaces": { "packages": [ diff --git a/packages/common/src/schema.ts b/packages/common/src/schema.ts index 64d4de66..72089104 100644 --- a/packages/common/src/schema.ts +++ b/packages/common/src/schema.ts @@ -38,30 +38,8 @@ export type Instance_Out = PocketbaseRecord & { status: InstanceStatus } -export type Instance_Internal_In = { - instanceId?: InstanceId - port?: Port - certCreatedAt?: IsoDate - nginxCreatedAt?: IsoDate - pid?: ProcessId - launchedAt?: IsoDate -} - -export type Instance_Internal_Out = PocketbaseRecord & { - instanceId: InstanceId - port: Port - certCreatedAt: IsoDate - nginxCreatedAt: IsoDate - pid: ProcessId - launchedAt: IsoDate -} - -export type Any_Record_Out = Instance_Out | Instance_Internal_Out +export type Any_Record_Out = Instance_Out export type Instance_Out_ByIdCollection = { [_: InstanceId]: Instance_Out } - -export type Instance_Internal_Out_ByIdCollection = { - [_: InstanceId]: Instance_Internal_Out -} diff --git a/packages/daemon/package.json b/packages/daemon/package.json index f43b44de..e8efe35f 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -4,8 +4,10 @@ "license": "MIT", "scripts": { "build": "mkdir -p dist && esbuild src/server.ts --bundle --platform=node > dist/server.js && echo 'Build complete' `date`", - "watch": "chokidar 'src/**' -c 'yarn build' --initial", - "serve": "node dist/server.js" + "dev": "yarn build && concurrently 'yarn dev:watch' 'yarn dev:serve'", + "dev:watch": "chokidar 'src/**' '../pocketbase/src/**' -c 'yarn build'", + "dev:serve": "nodemon dist/server.js", + "start": "node dist/server.js" }, "targets": { "server": { @@ -36,8 +38,10 @@ }, "devDependencies": { "chokidar-cli": "^3.0.0", + "concurrently": "^7.4.0", "esbuild": "^0.15.11", + "nodemon": "^2.0.20", "parcel": "^2.7.0", "ts-node": "^10.9.1" } -} +} \ No newline at end of file diff --git a/packages/daemon/src/InstanceManager.ts b/packages/daemon/src/InstanceManager.ts index c9c00c49..d08be513 100644 --- a/packages/daemon/src/InstanceManager.ts +++ b/packages/daemon/src/InstanceManager.ts @@ -1,17 +1,20 @@ import { InstanceStatus } from '@pockethost/common' -import { map } from '@s-libs/micro-dash' +import { forEachRight, map } from '@s-libs/micro-dash' import Bottleneck from 'bottleneck' import { ChildProcessWithoutNullStreams, spawn } from 'child_process' import getPort from 'get-port' import fetch from 'node-fetch' import { - APP_DOMAIN, - CORE_PB_PASSWORD, - CORE_PB_PORT, - CORE_PB_SUBDOMAIN, - CORE_PB_USERNAME, - PB_IDLE_TTL, + DAEMON_PB_BIN_DIR, + DAEMON_PB_DATA_DIR, + DAEMON_PB_IDLE_TTL, + DAEMON_PB_PASSWORD, + DAEMON_PB_PORT_BASE, + DAEMON_PB_USERNAME, + PUBLIC_APP_DOMAIN, + PUBLIC_PB_SUBDOMAIN, } from './constants' +import { collections_001 } from './migrations' import { createPbClient } from './PbClient' type Instance = { @@ -19,12 +22,10 @@ type Instance = { internalUrl: string port: number heartbeat: (shouldStop?: boolean) => void + shutdown: () => boolean + startRequest: () => () => void } -const ROOT_DIR = `/mount/pocketbase` -const BIN_ROOT = `${ROOT_DIR}/bin` -const INSTANCES_ROOT = `${ROOT_DIR}/instances` - const tryFetch = (url: string) => new Promise((resolve, reject) => { const tryFetch = () => { @@ -54,11 +55,12 @@ export const createInstanceManger = async () => { bin: string }) => { const { subdomain, port, bin } = cfg - const cmd = `${BIN_ROOT}/${bin}` + const cmd = `${DAEMON_PB_BIN_DIR}/${bin}` + const args = [ `serve`, `--dir`, - `${INSTANCES_ROOT}/${subdomain}/pb_data`, + `${DAEMON_PB_DATA_DIR}/${subdomain}/pb_data`, `--http`, mkInternalAddress(port), ] @@ -79,7 +81,9 @@ export const createInstanceManger = async () => { ls.on('exit', (code) => { instances[subdomain]?.heartbeat(true) delete instances[subdomain] - client.updateInstanceStatus(subdomain, InstanceStatus.Idle) + if (subdomain !== PUBLIC_PB_SUBDOMAIN) { + client.updateInstanceStatus(subdomain, InstanceStatus.Idle) + } console.log(`${subdomain} exited with code ${code}`) }) ls.on('error', (err) => { @@ -90,25 +94,31 @@ export const createInstanceManger = async () => { return ls } - const coreInternalUrl = mkInternalUrl(CORE_PB_PORT) + const coreInternalUrl = mkInternalUrl(DAEMON_PB_PORT_BASE) const client = createPbClient(coreInternalUrl) const mainProcess = await _spawn({ - subdomain: CORE_PB_SUBDOMAIN, - port: CORE_PB_PORT, + subdomain: PUBLIC_PB_SUBDOMAIN, + port: DAEMON_PB_PORT_BASE, bin: 'pocketbase', }) - instances[CORE_PB_SUBDOMAIN] = { + instances[PUBLIC_PB_SUBDOMAIN] = { process: mainProcess, internalUrl: coreInternalUrl, - port: CORE_PB_PORT, + port: DAEMON_PB_PORT_BASE, heartbeat: () => {}, + shutdown: () => { + console.log(`Shutting down instance ${PUBLIC_PB_SUBDOMAIN}`) + return mainProcess.kill() + }, + startRequest: () => () => {}, } await tryFetch(coreInternalUrl) try { - await client.adminAuthViaEmail(CORE_PB_USERNAME, CORE_PB_PASSWORD) + await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD) + await client.migrate(collections_001) } catch (e) { console.error( - `***WARNING*** CANNOT AUTHENTICATE TO https://${CORE_PB_SUBDOMAIN}.${APP_DOMAIN}/_/` + `***WARNING*** CANNOT AUTHENTICATE TO https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_APP_DOMAIN}/_/` ) console.error( `***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER` @@ -157,31 +167,64 @@ export const createInstanceManger = async () => { bin: instance.bin || 'pocketbase', }) - const internalUrl = mkInternalUrl(newPort) + const api: Instance = (() => { + let openRequestCount = 0 + const internalUrl = mkInternalUrl(newPort) - const api: Instance = { - process: childProcess, - internalUrl, - port: newPort, - heartbeat: (() => { - let tid: ReturnType - const _cleanup = () => { - childProcess.kill() - } - tid = setTimeout(_cleanup, PB_IDLE_TTL) - return (shouldStop) => { - clearTimeout(tid) - if (!shouldStop) { - tid = setTimeout(_cleanup, PB_IDLE_TTL) + const RECHECK_TTL = 1000 // 1 second + const _api: Instance = { + process: childProcess, + internalUrl, + port: newPort, + shutdown: () => { + console.log(`Shutting down instance ${subdomain}`) + return childProcess.kill() + }, + heartbeat: (() => { + let tid: ReturnType + const _cleanup = () => { + if (openRequestCount === 0) { + _api.shutdown() + } else { + console.log( + `${openRequestCount} requests remain open on ${subdomain}` + ) + tid = setTimeout(_cleanup, RECHECK_TTL) + } } - } - })(), - } + tid = setTimeout(_cleanup, DAEMON_PB_IDLE_TTL) + return (shouldStop) => { + clearTimeout(tid) + if (!shouldStop) { + tid = setTimeout(_cleanup, DAEMON_PB_IDLE_TTL) + } + } + })(), + startRequest: () => { + openRequestCount++ + const id = openRequestCount + + console.log(`${subdomain} started new request ${id}`) + return () => { + openRequestCount-- + console.log(`${subdomain} ended request ${id}`) + } + }, + } + return _api + })() + instances[subdomain] = api await client.updateInstanceStatus(subdomain, InstanceStatus.Running) - console.log(`${internalUrl} is running`) + console.log(`${api.internalUrl} is running`) return instances[subdomain] }) - return { getInstance } + const shutdown = () => { + console.log(`Shutting down instance manager`) + forEachRight(instances, (instance) => { + instance.shutdown() + }) + } + return { getInstance, shutdown } } diff --git a/packages/daemon/src/PbClient.ts b/packages/daemon/src/PbClient.ts index c876fc6c..341041fe 100644 --- a/packages/daemon/src/PbClient.ts +++ b/packages/daemon/src/PbClient.ts @@ -1,5 +1,6 @@ import { InstanceStatus } from '@pockethost/common' import PocketBase from 'pocketbase' +import { Collection_Serialized } from './migrations' const safeCatch = ( name: string, @@ -47,5 +48,17 @@ export const createPbClient = (url: string) => { } ) - return { adminAuthViaEmail, getInstanceBySubdomain, updateInstanceStatus } + const migrate = safeCatch( + `migrate`, + async (collections: Collection_Serialized[]) => { + await client.collections.import(collections) + } + ) + + return { + adminAuthViaEmail, + getInstanceBySubdomain, + updateInstanceStatus, + migrate, + } } diff --git a/packages/daemon/src/ProxyServer.ts b/packages/daemon/src/ProxyServer.ts index 95dbc979..47c279ce 100644 --- a/packages/daemon/src/ProxyServer.ts +++ b/packages/daemon/src/ProxyServer.ts @@ -37,9 +37,19 @@ export const createProxyServer = async () => { console.log( `Forwarding proxy request for ${req.url} to instance ${instance.internalUrl}` ) + const endRequest = instance.startRequest() + req.on('close', endRequest) proxy.web(req, res, { target: instance.internalUrl }) }) console.log('daemon on port 3000') server.listen(3000) + + const shutdown = () => { + console.log(`Shutting down proxy server`) + server.close() + instanceManager.shutdown() + } + + return { shutdown } } diff --git a/packages/daemon/src/constants.ts b/packages/daemon/src/constants.ts index e2a265ef..33a6361d 100644 --- a/packages/daemon/src/constants.ts +++ b/packages/daemon/src/constants.ts @@ -1,23 +1,50 @@ -export const APP_DOMAIN = process.env.APP_DOMAIN || `pockethost.local` -export const CORE_PB_SUBDOMAIN = - process.env.CORE_PB_SUBDOMAIN || `pockethost-central` -export const CORE_PB_USERNAME = (() => { - const v = process.env.CORE_PB_USERNAME +import { existsSync } from 'fs' + +export const PUBLIC_APP_DOMAIN = + process.env.PUBLIC_APP_DOMAIN || `pockethost.test` +export const PUBLIC_PB_SUBDOMAIN = + process.env.PUBLIC_PB_SUBDOMAIN || `pockethost-central` +export const DAEMON_PB_USERNAME = (() => { + const v = process.env.DAEMON_PB_USERNAME if (!v) { - throw new Error(`CORE_PB_USERNAME environment variable must be specified`) + throw new Error(`DAEMON_PB_USERNAME environment variable must be specified`) } return v })() -export const CORE_PB_PASSWORD = (() => { - const v = process.env.CORE_PB_PASSWORD +export const DAEMON_PB_PASSWORD = (() => { + const v = process.env.DAEMON_PB_PASSWORD if (!v) { - throw new Error(`CORE_PB_PASSWORD environment variable must be specified`) + throw new Error(`DAEMON_PB_PASSWORD environment variable must be specified`) } return v })() -export const CORE_PB_PORT = process.env.CORE_PB_PORT - ? parseInt(process.env.CORE_PB_PORT, 10) +export const DAEMON_PB_PORT_BASE = process.env.DAEMON_PB_PORT_BASE + ? parseInt(process.env.DAEMON_PB_PORT_BASE, 10) : 8090 -export const PB_IDLE_TTL = process.env.PB_IDLE_TTL - ? parseInt(process.env.PB_IDLE_TTL, 10) +export const DAEMON_PB_IDLE_TTL = process.env.DAEMON_PB_IDLE_TTL + ? parseInt(process.env.DAEMON_PB_IDLE_TTL, 10) : 5000 +export const DAEMON_PB_BIN_DIR = (() => { + const v = process.env.DAEMON_PB_BIN_DIR + if (!v) { + throw new Error( + `DAEMON_PB_BIN_DIR (${v}) environment variable must be specified` + ) + } + if (!existsSync(v)) { + throw new Error(`DAEMON_PB_BIN_DIR (${v}) path must exist`) + } + return v +})() +export const DAEMON_PB_DATA_DIR = (() => { + const v = process.env.DAEMON_PB_DATA_DIR + if (!v) { + throw new Error( + `DAEMON_PB_DATA_DIR (${v}) environment variable must be specified` + ) + } + if (!existsSync(v)) { + throw new Error(`DAEMON_PB_DATA_DIR (${v}) path must exist`) + } + return v +})() diff --git a/packages/daemon/src/migrations.ts b/packages/daemon/src/migrations.ts new file mode 100644 index 00000000..1e0ec40d --- /dev/null +++ b/packages/daemon/src/migrations.ts @@ -0,0 +1,128 @@ +import { Collection, SchemaField } from 'pocketbase' + +export type Collection_Serialized = Omit, 'schema'> & { + schema: Array> +} + +export const collections_001: Collection_Serialized[] = [ + { + id: 'systemprofiles0', + name: 'profiles', + system: true, + listRule: 'userId = @request.user.id', + viewRule: 'userId = @request.user.id', + createRule: 'userId = @request.user.id', + updateRule: 'userId = @request.user.id', + deleteRule: null, + schema: [ + { + id: 'pbfielduser', + name: 'userId', + type: 'user', + system: true, + required: true, + unique: true, + options: { + maxSelect: 1, + cascadeDelete: true, + }, + }, + { + id: 'pbfieldname', + name: 'name', + type: 'text', + system: false, + required: false, + unique: false, + options: { + min: null, + max: null, + pattern: '', + }, + }, + { + id: 'pbfieldavatar', + name: 'avatar', + type: 'file', + system: false, + required: false, + unique: false, + options: { + maxSelect: 1, + maxSize: 5242880, + mimeTypes: [ + 'image/jpg', + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'image/gif', + ], + thumbs: null, + }, + }, + ], + }, + { + id: 'etae8tuiaxl6xfv', + name: 'instances', + system: false, + listRule: 'uid=@request.user.id', + viewRule: 'uid = @request.user.id', + createRule: "uid = @request.user.id && (status = 'idle' || status = '')", + updateRule: null, + deleteRule: null, + schema: [ + { + id: 'qdtuuld1', + name: 'subdomain', + type: 'text', + system: false, + required: true, + unique: true, + options: { + min: null, + max: 50, + pattern: '^[a-z][\\-a-z]+$', + }, + }, + { + id: 'rbj14krn', + name: 'uid', + type: 'user', + system: false, + required: true, + unique: false, + options: { + maxSelect: 1, + cascadeDelete: false, + }, + }, + { + id: 'c2y74d7h', + name: 'status', + type: 'text', + system: false, + required: false, + unique: false, + options: { + min: null, + max: null, + pattern: '', + }, + }, + { + id: '3rinhcnt', + name: 'bin', + type: 'text', + system: false, + required: false, + unique: false, + options: { + min: 0, + max: 30, + pattern: '', + }, + }, + ], + }, +] diff --git a/packages/daemon/src/server.ts b/packages/daemon/src/server.ts index 9555c6a8..3c508329 100644 --- a/packages/daemon/src/server.ts +++ b/packages/daemon/src/server.ts @@ -1,2 +1,7 @@ import { createProxyServer } from './ProxyServer' -createProxyServer() +createProxyServer().then((api) => { + process.once('SIGUSR2', async () => { + console.log(`SIGUSR2 detected`) + api.shutdown() + }) +}) diff --git a/packages/daemon/yarn-error.log b/packages/daemon/yarn-error.log deleted file mode 100644 index 4e21d1b5..00000000 --- a/packages/daemon/yarn-error.log +++ /dev/null @@ -1,92 +0,0 @@ -Arguments: - /usr/local/bin/node /Users/meta/.yarn/bin/yarn.js - -PATH: - /Users/meta/.yarn/bin:/Users/meta/.config/yarn/global/node_modules/.bin:/Volumes/Code/butler-darwin-amd64:/Users/meta/.bun/bin:/Users/meta/Downloads/google-cloud-sdk/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Library/Apple/usr/bin - -Yarn version: - 1.22.19 - -Node version: - 18.9.0 - -Platform: - darwin arm64 - -Trace: - Error: https://registry.yarnpkg.com/@pockethost%2fcommon: Not found - at params.callback [as _callback] (/Users/meta/.yarn/lib/cli.js:66145:18) - at self.callback (/Users/meta/.yarn/lib/cli.js:140890:22) - at Request.emit (node:events:513:28) - at Request. (/Users/meta/.yarn/lib/cli.js:141862:10) - at Request.emit (node:events:513:28) - at IncomingMessage. (/Users/meta/.yarn/lib/cli.js:141784:12) - at Object.onceWrapper (node:events:627:28) - at IncomingMessage.emit (node:events:525:35) - at endReadableNT (node:internal/streams/readable:1359:12) - at process.processTicksAndRejections (node:internal/process/task_queues:82:21) - -npm manifest: - { - "name": "@pockethost/daemon", - "version": "0.0.1", - "license": "MIT", - "type": "module", - "scripts": { - "build": "parcel", - "watch": "parcel watch", - "serve": "ts-node src/server.ts" - }, - "targets": { - "server": { - "engines": { - "node": ">=18" - }, - "source": "src/server.ts", - "includeNodeModules": [ - "@pockethost/common", - "get-port", - "pocketbase", - "@s-libs/micro-dash" - ] - } - }, - "dependencies": { - "@pockethost/common": "0.0.1", - "@s-libs/micro-dash": "^14.1.0", - "@types/http-proxy": "^1.17.9", - "bottleneck": "^2.19.5", - "event-source-polyfill": "^1.0.31", - "get-port": "^6.1.2", - "http-proxy": "^1.18.1", - "pocketbase": "^0.7.0", - "ts-node": "^10.9.1" - }, - "devDependencies": { - "parcel": "^2.7.0", - "ts-node": "^10.9.1" - } - } - -yarn manifest: - No manifest - -Lockfile: - # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. - # yarn lockfile v1 - - - event-source-polyfill@^1.0.31: - version "1.0.31" - resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz#45fb0a6fc1375b2ba597361ba4287ffec5bf2e0c" - integrity sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA== - - get-port@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" - integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== - - pocketbase@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/pocketbase/-/pocketbase-0.7.0.tgz#bfce0225e03321cfa816f589a6dee60ec8b2f726" - integrity sha512-55le5D1dfthouWzxTIkL3eMgEsQqZsdT9O5HWFkCmbw889EKZsz0kf+2kkWgPvZqeVVXfOhMpJNHVOK5L2fJ3A== diff --git a/packages/examples/readme.md b/packages/examples/readme.md new file mode 100644 index 00000000..54e64d9e --- /dev/null +++ b/packages/examples/readme.md @@ -0,0 +1,5 @@ +# Example Apps + +Each example is designed to run against the Docker setup. + +For each example, the `docker-compose-dev.yaml` is already set up with a subdomain entry matching the example directory name. diff --git a/packages/pocketbase/src/pocketbase b/packages/pocketbase/src/pocketbase new file mode 100755 index 00000000..5bc92697 Binary files /dev/null and b/packages/pocketbase/src/pocketbase differ diff --git a/packages/pockethost.io/.env-template-docker b/packages/pockethost.io/.env-template-docker deleted file mode 100644 index f058bb3c..00000000 --- a/packages/pockethost.io/.env-template-docker +++ /dev/null @@ -1,3 +0,0 @@ -PUBLIC_APP_DOMAIN = localhost -PUBLIC_PB_SUBDOMAIN = pockethost-central -PUBLIC_PB_DOMAIN = pockethost.io diff --git a/packages/pockethost.io/.env-template-frontend-only b/packages/pockethost.io/.env-template-frontend-only deleted file mode 100644 index f058bb3c..00000000 --- a/packages/pockethost.io/.env-template-frontend-only +++ /dev/null @@ -1,3 +0,0 @@ -PUBLIC_APP_DOMAIN = localhost -PUBLIC_PB_SUBDOMAIN = pockethost-central -PUBLIC_PB_DOMAIN = pockethost.io diff --git a/packages/pockethost.io/.gitignore b/packages/pockethost.io/.gitignore index c230fbcd..d731ef9f 100644 --- a/packages/pockethost.io/.gitignore +++ b/packages/pockethost.io/.gitignore @@ -3,9 +3,6 @@ node_modules /build /.svelte-kit /package -.env -.env.* -!.env.example dist-server .idea yarn-error.log diff --git a/packages/pockethost.io/package.json b/packages/pockethost.io/package.json index 49f3f02a..b8dc6012 100644 --- a/packages/pockethost.io/package.json +++ b/packages/pockethost.io/package.json @@ -10,7 +10,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check .", "format": "prettier --write .", - "serve": "node dist-server/index.js", + "start": "node dist-server/index.js", "watch": "chokidar 'src/**' -c 'yarn build' --initial" }, "devDependencies": { @@ -33,4 +33,4 @@ "sass": "^1.54.9", "svelte-highlight": "^6.2.1" } -} +} \ No newline at end of file diff --git a/packages/pockethost.io/src/env.ts b/packages/pockethost.io/src/env.ts new file mode 100644 index 00000000..b2985cf1 --- /dev/null +++ b/packages/pockethost.io/src/env.ts @@ -0,0 +1,3 @@ +import { env } from '$env/dynamic/public' + +export const { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN, PUBLIC_APP_DOMAIN } = env diff --git a/packages/pockethost.io/src/pocketbase/index.ts b/packages/pockethost.io/src/pocketbase/index.ts index 8a15e817..93f085b2 100644 --- a/packages/pockethost.io/src/pocketbase/index.ts +++ b/packages/pockethost.io/src/pocketbase/index.ts @@ -1,4 +1,4 @@ -import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$env/static/public' +import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$src/env' import { createPocketbaseClient } from './PocketbaseClient' const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}` diff --git a/packages/pockethost.io/src/routes/+page.svelte b/packages/pockethost.io/src/routes/+page.svelte index 3e92f606..665c8cf6 100644 --- a/packages/pockethost.io/src/routes/+page.svelte +++ b/packages/pockethost.io/src/routes/+page.svelte @@ -2,6 +2,7 @@ import FeatureCard from '$components/FeatureCard.svelte' import HomepageHeroAnimation from '$components/HomepageHeroAnimation.svelte' import InstanceGeneratorWidget from '$components/InstanceGeneratorWidget.svelte' + import { PUBLIC_APP_DOMAIN } from '$src/env' import { client } from '$src/pocketbase' const { isLoggedIn } = client @@ -85,7 +86,7 @@

Email and oAuth authentication options work out of the box. Send transactional email to your users from our verified domain and your custom address yoursubdomin@pockethost.localyoursubdomin@{PUBLIC_APP_DOMAIN}.

@@ -133,7 +134,7 @@
  • JS/TS cloud functions
  • Deploy to Fly.io
  • -
  • Lightstream
  • +
  • Litestream
diff --git a/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte b/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte index 43d11f25..f097ba76 100644 --- a/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte +++ b/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte @@ -2,8 +2,8 @@ import { page } from '$app/stores' import CodeSample from '$components/CodeSample.svelte' import Protected from '$components/Protected.svelte' + import { PUBLIC_PB_DOMAIN } from '$src/env' import ProvisioningStatus from '$components/ProvisioningStatus.svelte' - import { PUBLIC_PB_DOMAIN } from '$env/static/public' import { client } from '$src/pocketbase' import { assertExists } from '@pockethost/common/src/assert' import type { Instance_Out } from '@pockethost/common/src/schema' diff --git a/packages/pockethost.io/src/routes/app/new/+page.svelte b/packages/pockethost.io/src/routes/app/new/+page.svelte index 3a3d7001..a5a27f57 100644 --- a/packages/pockethost.io/src/routes/app/new/+page.svelte +++ b/packages/pockethost.io/src/routes/app/new/+page.svelte @@ -1,9 +1,9 @@ - {#if hasPageLoaded} -
- {#if values(apps).length} -
-

Your Apps

-
+
+ {#if values(apps).length} +
+

Your Apps

+
-
- {#each values(apps) as app} -
-
-
- -
+
+ {#each values(apps) as app} +
+
+
+ +
-

{app.subdomain}

+

{app.subdomain}

-
- {/each} -
- {/if} - -
- -
-

Create Your {isFirstApplication ? 'First' : 'Next'} App

- New App
-
+ {/each}
+ {/if} + +
+ +
+

Create Your {isFirstApplication ? 'First' : 'Next'} App

+ New App +
+
- {/if} +