diff --git a/.dockerignore b/.dockerignore index 91e33f0..0c98206 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,7 +14,11 @@ build # Environment .env +.env.test +**/.env +**/.env.test .env*.local +**/.env*.local # Misc .DS_Store diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..639d75d --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,18 @@ +# Production Docker Compose environment. +# Copy to .env.prod and replace every example secret/domain before deploying. + +# PostgreSQL +POSTGRES_USER=loreo +POSTGRES_PASSWORD=replace-with-a-random-database-password +POSTGRES_DB=loreo + +# Published host ports. The server still listens on 3000 inside the Docker network. +SERVER_PUBLIC_PORT=3002 +WEB_PUBLIC_PORT=3001 + +# Server +CORS_ORIGINS=https://loreo.example.com +JWT_SECRET=replace-with-a-random-secret-at-least-32-characters + +# Public web URL used by the server when generating local storage URLs. +PUBLIC_URL=https://loreo.example.com diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2727d3a..f471c39 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -33,8 +33,7 @@ jobs: include: - image: loreo-web dockerfile: apps/web/Dockerfile.prod - build-args: | - VITE_API_URL=${{ vars.VITE_API_URL || 'http://localhost:3001' }} + build-args: '' - image: loreo-server dockerfile: apps/server/Dockerfile.prod build-args: '' diff --git a/apps/web/.env.example b/apps/web/.env.example index 5317fce..c2516bd 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1 +1,2 @@ +# Optional. Leave unset for same-origin API requests through the web nginx proxy. VITE_API_URL=http://localhost:3000 diff --git a/apps/web/.env.test.example b/apps/web/.env.test.example index 5317fce..c2516bd 100644 --- a/apps/web/.env.test.example +++ b/apps/web/.env.test.example @@ -1 +1,2 @@ +# Optional. Leave unset for same-origin API requests through the web nginx proxy. VITE_API_URL=http://localhost:3000 diff --git a/apps/web/Dockerfile.prod b/apps/web/Dockerfile.prod index a5ecc78..2ee8d6a 100644 --- a/apps/web/Dockerfile.prod +++ b/apps/web/Dockerfile.prod @@ -14,15 +14,14 @@ COPY apps/web ./apps/web WORKDIR /app/apps/web -ARG VITE_API_URL -ENV VITE_API_URL=$VITE_API_URL - RUN pnpm build FROM nginx:alpine AS runner COPY --from=builder /app/apps/web/dist /usr/share/nginx/html -COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +COPY apps/web/nginx.conf.template /etc/nginx/templates/default.conf.template + +ENV API_UPSTREAM=http://loreo-server:3000 EXPOSE 80 diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf.template similarity index 94% rename from apps/web/nginx.conf rename to apps/web/nginx.conf.template index 5ca9d62..db4c088 100644 --- a/apps/web/nginx.conf +++ b/apps/web/nginx.conf.template @@ -8,7 +8,7 @@ server { gzip_static on; location ~ ^/(health|auth|home|links|highlights|tags|files|imports|doc|reference|openapi\.json) { - proxy_pass http://loreo-server:3000; + proxy_pass ${API_UPSTREAM}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 0ada3ec..274bb72 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -49,7 +49,7 @@ const buildPayload = (body?: RequestBody, headers: object = {}): Options => { export const instance = ky.create({ credentials: 'include', - prefix: env.API_URL, + prefix: env.API_URL || globalThis.location.origin, retry: { limit: 1, statusCodes: [401, 403, 429, 500, 502, 503, 504] diff --git a/apps/web/src/lib/env.test.ts b/apps/web/src/lib/env.test.ts index fc5bb22..bbe2f01 100644 --- a/apps/web/src/lib/env.test.ts +++ b/apps/web/src/lib/env.test.ts @@ -1,11 +1,16 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -async function importEnv(demoMode?: 'true' | 'false') { +type ImportEnvOptions = { + apiUrl?: string; + demoMode?: 'true' | 'false'; +}; + +async function importEnv(options: ImportEnvOptions = {}) { vi.resetModules(); vi.unstubAllEnvs(); - vi.stubEnv('VITE_API_URL', 'http://localhost:3000'); - if (demoMode) { - vi.stubEnv('VITE_DEMO_MODE', demoMode); + vi.stubEnv('VITE_API_URL', options.apiUrl ?? ''); + if (options.demoMode) { + vi.stubEnv('VITE_DEMO_MODE', options.demoMode); } return import('./env'); @@ -24,8 +29,20 @@ describe('web env', () => { }); it('parses demo mode on', async () => { - const { env } = await importEnv('true'); + const { env } = await importEnv({ demoMode: 'true' }); expect(env.isDemo).toBe(true); }); + + it('defaults API URL to same-origin', async () => { + const { env } = await importEnv(); + + expect(env.API_URL).toBe(''); + }); + + it('allows API URL override', async () => { + const { env } = await importEnv({ apiUrl: 'https://api.example.com' }); + + expect(env.API_URL).toBe('https://api.example.com'); + }); }); diff --git a/apps/web/src/lib/env.ts b/apps/web/src/lib/env.ts index 2c94acb..27a43f8 100644 --- a/apps/web/src/lib/env.ts +++ b/apps/web/src/lib/env.ts @@ -1,7 +1,7 @@ import * as z from 'zod'; const envSchema = z.object({ - API_URL: z.string(), + API_URL: z.string().default(''), DEMO_MODE: z .enum(['true', 'false']) .default('false') diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index 9bb54f9..bf21171 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -1,7 +1,7 @@ /// interface ImportMetaEnv { - readonly VITE_API_URL: string; + readonly VITE_API_URL?: string; // more env variables... } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 35f9358..b8b0ad8 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,6 +1,6 @@ services: loreo-postgres: - image: postgres:16-alpine + image: postgres:17-alpine restart: always environment: POSTGRES_USER: ${POSTGRES_USER} @@ -42,7 +42,7 @@ services: image: ghcr.io/technowizard/loreo-server:latest restart: always ports: - - '3000:3000' + - '${SERVER_PUBLIC_PORT:-3000}:3000' environment: NODE_ENV: production DATABASE_HOST: loreo-postgres @@ -73,15 +73,12 @@ services: condition: service_started loreo-web: - build: - context: . - dockerfile: apps/web/Dockerfile.prod - args: - VITE_API_URL: ${VITE_API_URL:-http://localhost:3001} image: ghcr.io/technowizard/loreo-web:latest restart: always ports: - - '3001:80' + - '${WEB_PUBLIC_PORT:-3001}:80' + environment: + API_UPSTREAM: http://loreo-server:3000 networks: - frontend depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 232ca16..e1e5504 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: postgres: - image: postgres:16-alpine + image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER}