Svelte + PocketBase + Nginx = Amazingness
Tags: webdev, svelte, typescript, pocketbase
Published: 2023-08-18 10:54:17

So it's time for the promised bonus, how to setup Svelte with PocketBase on the same server using Nginx as proxy.

This stack is perfect for any small to medium sized applications, particularly considering Svelte connecting directly to PocketBase and PocketBase's ability to work without remote SQL due to local SQLite. The performance is incredible. You can do thousands of request, and you won't even notice it.

All the code is available here:
https://github.com/mpiorowski/svelte-auth

We will be using `example.com` as our targeted domain.

First let's look at the architecture:

![Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7z1gkkf9l2yeuj1j5c0g.png)

The general idea is for the Nginx server to act as a proxy. It directs all `/pb` calls to the PocketBase server, while forwarding the rest to the SvelteKit server.

**Folder structure**
```
/app
/pb
.nginx.config
docker-compose.yml
```

Everything is encapsuled using docker, and the easiest way to do it is by using `docker-compose`.

**docker-compose.yml**
```yml
version: "3"
services:
  app:
    container_name: svelte-auth-application
    working_dir: /app
    build:
      context: ./app

  pb:
    container_name: svelte-auth-pocketbase
    working_dir: /pb
    build:
      context: ./pb
    volumes:
      - ./pb/pb_data:/pb/pb_data
      - ./pb/pb_migrations:/pb/pb_migrations

  nginx:
    container_name: svelte-auth-nginx
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - /etc/letsencrypt:/etc/letsencrypt
    ports:
      - 80:80
      - 443:443
```

Thanks to the use of docker-compose, the Docker network is automatically created. This is why services can communicate with each other using their respective service names, such as `http://pb:8080` or `http://app:3000`.

Some key points to highlight:

- Only Nginx exposes ports to the external world.
- Nginx volumes are linked to the configuration file and certificate location.
- PocketBase volumes are linked to the SQLite database and migration files."

Now, concerning SQLite files, the `pb_data` folder is typically ignored in the repository, while the `pb_migrations` are included.

Additionally, another useful SQLite feature: need to back up the database? Simply copy the `pb_data` folder :)

Let's dive into Dockerfiles.

**pb/Dockerfile**

```bash
FROM alpine:latest

ARG PB_VERSION=0.16.5

RUN apk add --no-cache \
    unzip \
    ca-certificates

ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip
RUN unzip /tmp/pb.zip -d /pb/

CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8080"]
```

**app/Dockerfile**

```bash
# Development
FROM node:19-alpine as dev

WORKDIR /app
COPY package.json /app/package.json

RUN npm install -g pnpm
COPY pnpm-lock.yaml /app/pnpm-lock.yaml
RUN pnpm install

COPY . .

CMD ["pnpm", "dev"]

# Build
FROM node:19-alpine as build
WORKDIR /app
COPY . .

RUN npm install -g pnpm
RUN pnpm install
RUN pnpm build

# Production
FROM node:19-alpine as prod
WORKDIR /app
COPY --from=build /app/build /app/build
COPY src /app/src
COPY package.json /app/package.json
COPY pnpm-lock.yaml /app/pnpm-lock.yaml

RUN npm install -g pnpm
RUN pnpm install --prod

CMD node build
```

For those who are wondering about the `Build` and `Production` sections, these utilize a Docker multi-stage feature. This feature enables you to divide the build process and ultimately generate smaller Docker images.

https://docs.docker.com/build/building/multi-stage/

Now there is only one thing left:

**nginx.conf**

```bash
worker_processes 1;

events { worker_connections 1024; }

http {

    sendfile on;

    upstream docker-app {
        server app:3000;
    }

    upstream docker-pb {
        server pb:8080;
    }

    server {
        listen [::]:443 ssl;
        listen 443 ssl;
        server_name example.com;
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

        location / {
            proxy_pass         http://docker-app;
            proxy_redirect     off;
            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-Host $server_name;
        }

        location /pb/_/ {
            proxy_pass         http://docker-pb/_/;
            proxy_redirect     off;
            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-Host $server_name;
        }

        location /pb/api/ {
            proxy_pass         http://docker-pb/api/;
            proxy_redirect     off;
            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-Host $server_name;
        }
    }

    server {
        listen 80;
        server_name example.com;
        return 301 https://$host$request_uri;
    }
}
```

This one is doing the important work. Firstly, it sets up an upstream to our services (take note of the app and pb service names). Then, it effectively distributes traffic based on the requested URL. Additionally, it redirects all traffic from HTTP to HTTPS.

You might notice the `managed by Certbot` comments.  As we are exclusively running SSL, valid certificates are required on the server. The simplest approach to achieving this is by utilizing Certbot.

So on the server we need to run:

```bash
sudo certbot certonly --nginx -d example.com
sudo systemctl disable nginx
```

This not only generates valid certificates but also creates the appropriate configuration for our Nginx server. We simply need to disable the default server and use the one within the Docker environment.

And that's it! One server with both PocketBase and SvelteKit irunning.

PocketBase Admin panel: `example.com/pb/_/`
PocketBase Api: `example.com/pb/api/`
SvelteKit: `example.com`

One last thing to remember: when making client-side requests to PocketBase, the target should be `https://example.com/pb/api/`. However, for server-side connections within the Docker environment, we must keep in mind that the target should be `http://pb:8080`. This part can be a bit confusing :).

Hope You will like it like I do! Spread the love for SQLite!

Also, like with all the articles, a shameless advertisment:

[Follow me on Twitter](https://twitter.com/mapiorowski)  to receive notifications. I'm also working on promoting lesser-known technologies, with Svelte, Rust, and Go being some of the main focuses.