mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-14 08:42:08 +10:00
fix(server): 👷 pass errors down to controller
This commit is contained in:
@ -18,7 +18,7 @@ import { UtilsService } from "../utils/utils.service";
|
|||||||
|
|
||||||
const MM_TO_PX = 3.78;
|
const MM_TO_PX = 3.78;
|
||||||
const PREVIEW_TIMEOUT = 5000; // 5 seconds
|
const PREVIEW_TIMEOUT = 5000; // 5 seconds
|
||||||
const PRINTER_TIMEOUT = 15000; // 15 seconds
|
const PRINTER_TIMEOUT = 10000; // 10 seconds
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrinterService {
|
export class PrinterService {
|
||||||
@ -89,220 +89,212 @@ export class PrinterService {
|
|||||||
async generateResume(resume: ResumeDto) {
|
async generateResume(resume: ResumeDto) {
|
||||||
const browser = await this.getBrowser();
|
const browser = await this.getBrowser();
|
||||||
|
|
||||||
try {
|
const page = await browser.newPage();
|
||||||
const page = await browser.newPage();
|
|
||||||
|
|
||||||
let url = this.utils.getUrl();
|
let url = this.utils.getUrl();
|
||||||
const publicUrl = this.configService.getOrThrow<string>("PUBLIC_URL");
|
const publicUrl = this.configService.getOrThrow<string>("PUBLIC_URL");
|
||||||
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
|
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
|
||||||
|
|
||||||
if ([publicUrl, storageUrl].some((url) => url.includes("localhost"))) {
|
if ([publicUrl, storageUrl].some((url) => url.includes("localhost"))) {
|
||||||
// Switch client URL from `localhost` to `host.docker.internal` in development
|
// Switch client URL from `localhost` to `host.docker.internal` in development
|
||||||
// This is required because the browser is running in a container and the client is running on the host machine.
|
// This is required because the browser is running in a container and the client is running on the host machine.
|
||||||
url = url.replace("localhost", "host.docker.internal");
|
url = url.replace("localhost", "host.docker.internal");
|
||||||
|
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
|
||||||
// Intercept requests of `localhost` to `host.docker.internal` in development
|
// Intercept requests of `localhost` to `host.docker.internal` in development
|
||||||
page.on("request", (request) => {
|
page.on("request", (request) => {
|
||||||
if (request.url().startsWith(storageUrl)) {
|
if (request.url().startsWith(storageUrl)) {
|
||||||
const modifiedUrl = request.url().replace("localhost", `host.docker.internal`);
|
const modifiedUrl = request.url().replace("localhost", `host.docker.internal`);
|
||||||
|
|
||||||
request.continue({ url: modifiedUrl });
|
request.continue({ url: modifiedUrl });
|
||||||
} else {
|
} else {
|
||||||
request.continue();
|
request.continue();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Set the data of the resume to be printed in the browser's session storage
|
|
||||||
const format = resume.data.metadata.page.format;
|
|
||||||
const numPages = resume.data.metadata.layout.length;
|
|
||||||
|
|
||||||
await page.evaluateOnNewDocument((data: string) => {
|
|
||||||
sessionStorage.setItem("resume", data);
|
|
||||||
}, JSON.stringify(resume.data));
|
|
||||||
|
|
||||||
await page.goto(`${url}/printer`, { waitUntil: "networkidle0" });
|
|
||||||
await page.emulateMediaType("print");
|
|
||||||
|
|
||||||
const pagesBuffer: Buffer[] = [];
|
|
||||||
|
|
||||||
// Hide all the pages (elements with [data-page] attribute) using CSS
|
|
||||||
const hidePages = () => {
|
|
||||||
return page.$eval("iframe", (frame) => {
|
|
||||||
frame.contentDocument?.documentElement.querySelectorAll("[data-page]").forEach((page) => {
|
|
||||||
page.setAttribute("style", "display: none");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const processPage = (index: number) => {
|
|
||||||
// Calculate the height of the page based on the format, convert mm to pixels
|
|
||||||
const pageSize = {
|
|
||||||
width: pageSizeMap[format].width * MM_TO_PX,
|
|
||||||
height: pageSizeMap[format].height * MM_TO_PX,
|
|
||||||
};
|
|
||||||
|
|
||||||
return page.$eval(
|
|
||||||
"iframe",
|
|
||||||
(frame, index, pageSize) => {
|
|
||||||
const page = frame.contentDocument?.querySelector(`[data-page="${index}"]`);
|
|
||||||
page?.setAttribute("style", "display: block");
|
|
||||||
|
|
||||||
return {
|
|
||||||
width: Math.max(pageSize.width, page?.scrollWidth ?? 0),
|
|
||||||
height: Math.max(pageSize.height, page?.scrollHeight ?? 0),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
index,
|
|
||||||
pageSize,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Loop through all the pages and print them, by first displaying them, printing the PDF and then hiding them back
|
|
||||||
for (let index = 1; index <= numPages; index++) {
|
|
||||||
await hidePages();
|
|
||||||
|
|
||||||
const { width, height } = await processPage(index);
|
|
||||||
const buffer = await page.pdf({ width, height });
|
|
||||||
pagesBuffer.push(buffer);
|
|
||||||
|
|
||||||
await hidePages();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using 'pdf-lib', merge all the pages from their buffers into a single PDF
|
|
||||||
const pdf = await PDFDocument.create();
|
|
||||||
pdf.registerFontkit(fontkit);
|
|
||||||
|
|
||||||
// Get information about fonts used in the resume from the metadata
|
|
||||||
const fontData = resume.data.metadata.typography.font;
|
|
||||||
|
|
||||||
// Handle Special Case for CMU Serif as it is not available on Google Fonts
|
|
||||||
if (fontData.family === "CMU Serif") {
|
|
||||||
const fontsBuffer = await Promise.all([
|
|
||||||
readFile(join(__dirname, "assets/fonts/computer-modern/regular.ttf")),
|
|
||||||
readFile(join(__dirname, "assets/fonts/computer-modern/italic.ttf")),
|
|
||||||
readFile(join(__dirname, "assets/fonts/computer-modern/bold.ttf")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
fontsBuffer.map((buffer) => {
|
|
||||||
// Convert Buffer to ArrayBuffer
|
|
||||||
const arrayBuffer = buffer.buffer.slice(
|
|
||||||
buffer.byteOffset,
|
|
||||||
buffer.byteOffset + buffer.byteLength,
|
|
||||||
);
|
|
||||||
return pdf.embedFont(arrayBuffer);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const fontUrls = getFontUrls(fontData.family, fontData.variants);
|
|
||||||
|
|
||||||
// Load all the fonts from the URLs using HttpService
|
|
||||||
const responses = await Promise.all(
|
|
||||||
fontUrls.map((url) =>
|
|
||||||
this.httpService.axiosRef.get(url, {
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const fontsBuffer = responses.map((response) => response.data as ArrayBuffer);
|
|
||||||
|
|
||||||
// Embed all the fonts in the PDF
|
|
||||||
await Promise.all(fontsBuffer.map((buffer) => pdf.embedFont(buffer)));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let index = 0; index < pagesBuffer.length; index++) {
|
|
||||||
const page = await PDFDocument.load(pagesBuffer[index]);
|
|
||||||
const copiedPage = await pdf.copyPages(page, [0]);
|
|
||||||
pdf.addPage(copiedPage[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the PDF to storage and return the URL to download the resume
|
|
||||||
// Store the URL in cache for future requests, under the previously generated hash digest
|
|
||||||
const buffer = Buffer.from(await pdf.save());
|
|
||||||
|
|
||||||
// This step will also save the resume URL in cache
|
|
||||||
const resumeUrl = await this.storageService.uploadObject(
|
|
||||||
resume.userId,
|
|
||||||
"resumes",
|
|
||||||
buffer,
|
|
||||||
resume.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Close all the pages and disconnect from the browser
|
|
||||||
await page.close();
|
|
||||||
browser.disconnect();
|
|
||||||
|
|
||||||
return resumeUrl;
|
|
||||||
} catch (error) {
|
|
||||||
throw new InternalServerErrorException(ErrorMessage.ResumePrinterError, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the data of the resume to be printed in the browser's session storage
|
||||||
|
const format = resume.data.metadata.page.format;
|
||||||
|
const numPages = resume.data.metadata.layout.length;
|
||||||
|
|
||||||
|
await page.evaluateOnNewDocument((data: string) => {
|
||||||
|
sessionStorage.setItem("resume", data);
|
||||||
|
}, JSON.stringify(resume.data));
|
||||||
|
|
||||||
|
await page.goto(`${url}/printer`, { waitUntil: "networkidle0" });
|
||||||
|
await page.emulateMediaType("print");
|
||||||
|
|
||||||
|
const pagesBuffer: Buffer[] = [];
|
||||||
|
|
||||||
|
// Hide all the pages (elements with [data-page] attribute) using CSS
|
||||||
|
const hidePages = () => {
|
||||||
|
return page.$eval("iframe", (frame) => {
|
||||||
|
frame.contentDocument?.documentElement.querySelectorAll("[data-page]").forEach((page) => {
|
||||||
|
page.setAttribute("style", "display: none");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const processPage = (index: number) => {
|
||||||
|
// Calculate the height of the page based on the format, convert mm to pixels
|
||||||
|
const pageSize = {
|
||||||
|
width: pageSizeMap[format].width * MM_TO_PX,
|
||||||
|
height: pageSizeMap[format].height * MM_TO_PX,
|
||||||
|
};
|
||||||
|
|
||||||
|
return page.$eval(
|
||||||
|
"iframe",
|
||||||
|
(frame, index, pageSize) => {
|
||||||
|
const page = frame.contentDocument?.querySelector(`[data-page="${index}"]`);
|
||||||
|
page?.setAttribute("style", "display: block");
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: Math.max(pageSize.width, page?.scrollWidth ?? 0),
|
||||||
|
height: Math.max(pageSize.height, page?.scrollHeight ?? 0),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
index,
|
||||||
|
pageSize,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loop through all the pages and print them, by first displaying them, printing the PDF and then hiding them back
|
||||||
|
for (let index = 1; index <= numPages; index++) {
|
||||||
|
await hidePages();
|
||||||
|
|
||||||
|
const { width, height } = await processPage(index);
|
||||||
|
const buffer = await page.pdf({ width, height });
|
||||||
|
pagesBuffer.push(buffer);
|
||||||
|
|
||||||
|
await hidePages();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using 'pdf-lib', merge all the pages from their buffers into a single PDF
|
||||||
|
const pdf = await PDFDocument.create();
|
||||||
|
pdf.registerFontkit(fontkit);
|
||||||
|
|
||||||
|
// Get information about fonts used in the resume from the metadata
|
||||||
|
const fontData = resume.data.metadata.typography.font;
|
||||||
|
|
||||||
|
// Handle Special Case for CMU Serif as it is not available on Google Fonts
|
||||||
|
if (fontData.family === "CMU Serif") {
|
||||||
|
const fontsBuffer = await Promise.all([
|
||||||
|
readFile(join(__dirname, "assets/fonts/computer-modern/regular.ttf")),
|
||||||
|
readFile(join(__dirname, "assets/fonts/computer-modern/italic.ttf")),
|
||||||
|
readFile(join(__dirname, "assets/fonts/computer-modern/bold.ttf")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
fontsBuffer.map((buffer) => {
|
||||||
|
// Convert Buffer to ArrayBuffer
|
||||||
|
const arrayBuffer = buffer.buffer.slice(
|
||||||
|
buffer.byteOffset,
|
||||||
|
buffer.byteOffset + buffer.byteLength,
|
||||||
|
);
|
||||||
|
return pdf.embedFont(arrayBuffer);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const fontUrls = getFontUrls(fontData.family, fontData.variants);
|
||||||
|
|
||||||
|
// Load all the fonts from the URLs using HttpService
|
||||||
|
const responses = await Promise.all(
|
||||||
|
fontUrls.map((url) =>
|
||||||
|
this.httpService.axiosRef.get(url, {
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const fontsBuffer = responses.map((response) => response.data as ArrayBuffer);
|
||||||
|
|
||||||
|
// Embed all the fonts in the PDF
|
||||||
|
await Promise.all(fontsBuffer.map((buffer) => pdf.embedFont(buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < pagesBuffer.length; index++) {
|
||||||
|
const page = await PDFDocument.load(pagesBuffer[index]);
|
||||||
|
const copiedPage = await pdf.copyPages(page, [0]);
|
||||||
|
pdf.addPage(copiedPage[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the PDF to storage and return the URL to download the resume
|
||||||
|
// Store the URL in cache for future requests, under the previously generated hash digest
|
||||||
|
const buffer = Buffer.from(await pdf.save());
|
||||||
|
|
||||||
|
// This step will also save the resume URL in cache
|
||||||
|
const resumeUrl = await this.storageService.uploadObject(
|
||||||
|
resume.userId,
|
||||||
|
"resumes",
|
||||||
|
buffer,
|
||||||
|
resume.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close all the pages and disconnect from the browser
|
||||||
|
await page.close();
|
||||||
|
browser.disconnect();
|
||||||
|
|
||||||
|
return resumeUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generatePreview(resume: ResumeDto) {
|
async generatePreview(resume: ResumeDto) {
|
||||||
const browser = await this.getBrowser();
|
const browser = await this.getBrowser();
|
||||||
|
|
||||||
try {
|
const page = await browser.newPage();
|
||||||
const page = await browser.newPage();
|
|
||||||
|
|
||||||
let url = this.utils.getUrl();
|
let url = this.utils.getUrl();
|
||||||
const publicUrl = this.configService.getOrThrow<string>("PUBLIC_URL");
|
const publicUrl = this.configService.getOrThrow<string>("PUBLIC_URL");
|
||||||
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
|
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
|
||||||
|
|
||||||
if ([publicUrl, storageUrl].some((url) => url.includes("localhost"))) {
|
if ([publicUrl, storageUrl].some((url) => url.includes("localhost"))) {
|
||||||
// Switch client URL from `localhost` to `host.docker.internal` in development
|
// Switch client URL from `localhost` to `host.docker.internal` in development
|
||||||
// This is required because the browser is running in a container and the client is running on the host machine.
|
// This is required because the browser is running in a container and the client is running on the host machine.
|
||||||
url = url.replace("localhost", "host.docker.internal");
|
url = url.replace("localhost", "host.docker.internal");
|
||||||
|
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
|
||||||
// Intercept requests of `localhost` to `host.docker.internal` in development
|
// Intercept requests of `localhost` to `host.docker.internal` in development
|
||||||
page.on("request", (request) => {
|
page.on("request", (request) => {
|
||||||
if (request.url().startsWith(storageUrl)) {
|
if (request.url().startsWith(storageUrl)) {
|
||||||
const modifiedUrl = request.url().replace("localhost", `host.docker.internal`);
|
const modifiedUrl = request.url().replace("localhost", `host.docker.internal`);
|
||||||
|
|
||||||
request.continue({ url: modifiedUrl });
|
request.continue({ url: modifiedUrl });
|
||||||
} else {
|
} else {
|
||||||
request.continue();
|
request.continue();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the data of the resume to be printed in the browser's session storage
|
|
||||||
const format = resume.data.metadata.page.format;
|
|
||||||
|
|
||||||
await page.evaluateOnNewDocument((data: string) => {
|
|
||||||
sessionStorage.setItem("resume", data);
|
|
||||||
}, JSON.stringify(resume.data));
|
|
||||||
|
|
||||||
await page.setViewport({
|
|
||||||
width: Math.round(pageSizeMap[format].width * MM_TO_PX),
|
|
||||||
height: Math.round(pageSizeMap[format].height * MM_TO_PX),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`${url}/printer`, { waitUntil: "networkidle0" });
|
|
||||||
|
|
||||||
// Save the JPEG to storage and return the URL
|
|
||||||
// Store the URL in cache for future requests, under the previously generated hash digest
|
|
||||||
const buffer = await page.screenshot({ quality: 80, type: "jpeg" });
|
|
||||||
|
|
||||||
// Generate a hash digest of the resume data, this hash will be used to check if the resume has been updated
|
|
||||||
const previewUrl = await this.storageService.uploadObject(
|
|
||||||
resume.userId,
|
|
||||||
"previews",
|
|
||||||
buffer,
|
|
||||||
resume.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Close all the pages and disconnect from the browser
|
|
||||||
await page.close();
|
|
||||||
browser.disconnect();
|
|
||||||
|
|
||||||
return previewUrl;
|
|
||||||
} catch (error) {
|
|
||||||
throw new InternalServerErrorException(ErrorMessage.ResumePreviewError, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the data of the resume to be printed in the browser's session storage
|
||||||
|
const format = resume.data.metadata.page.format;
|
||||||
|
|
||||||
|
await page.evaluateOnNewDocument((data: string) => {
|
||||||
|
sessionStorage.setItem("resume", data);
|
||||||
|
}, JSON.stringify(resume.data));
|
||||||
|
|
||||||
|
await page.setViewport({
|
||||||
|
width: Math.round(pageSizeMap[format].width * MM_TO_PX),
|
||||||
|
height: Math.round(pageSizeMap[format].height * MM_TO_PX),
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`${url}/printer`, { waitUntil: "networkidle0" });
|
||||||
|
|
||||||
|
// Save the JPEG to storage and return the URL
|
||||||
|
// Store the URL in cache for future requests, under the previously generated hash digest
|
||||||
|
const buffer = await page.screenshot({ quality: 80, type: "jpeg" });
|
||||||
|
|
||||||
|
// Generate a hash digest of the resume data, this hash will be used to check if the resume has been updated
|
||||||
|
const previewUrl = await this.storageService.uploadObject(
|
||||||
|
resume.userId,
|
||||||
|
"previews",
|
||||||
|
buffer,
|
||||||
|
resume.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close all the pages and disconnect from the browser
|
||||||
|
await page.close();
|
||||||
|
browser.disconnect();
|
||||||
|
|
||||||
|
return previewUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,7 +125,10 @@ export class ResumeController {
|
|||||||
|
|
||||||
return { url };
|
return { url };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new InternalServerErrorException(ErrorMessage.ResumePrinterError, error);
|
throw new InternalServerErrorException(ErrorMessage.ResumePrinterError, {
|
||||||
|
cause: error,
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +141,10 @@ export class ResumeController {
|
|||||||
|
|
||||||
return { url };
|
return { url };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new InternalServerErrorException(ErrorMessage.ResumePreviewError);
|
throw new InternalServerErrorException(ErrorMessage.ResumePreviewError, {
|
||||||
|
cause: error,
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ version: "3"
|
|||||||
services:
|
services:
|
||||||
# Database (Postgres)
|
# Database (Postgres)
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- ${POSTGRES_PORT:-5432}:5432
|
- ${POSTGRES_PORT:-5432}:5432
|
||||||
@ -18,7 +18,7 @@ services:
|
|||||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@ -38,12 +38,6 @@ services:
|
|||||||
MINIO_CONSOLE_ADDRESS: :9001
|
MINIO_CONSOLE_ADDRESS: :9001
|
||||||
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY:-minioadmin}
|
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY:-minioadmin}
|
||||||
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY:-minioadmin}
|
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY:-minioadmin}
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl -f http://minio:9000/minio/health/live"]
|
|
||||||
start_period: 40s
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# Chrome Browser (for printing and previews)
|
# Chrome Browser (for printing and previews)
|
||||||
chrome:
|
chrome:
|
||||||
@ -58,7 +52,7 @@ services:
|
|||||||
|
|
||||||
# Redis (for cache & server session management)
|
# Redis (for cache & server session management)
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD:-password}
|
command: redis-server --requirepass ${REDIS_PASSWORD:-password}
|
||||||
ports:
|
ports:
|
||||||
@ -7,7 +7,7 @@ version: "3"
|
|||||||
services:
|
services:
|
||||||
# Database (Postgres)
|
# Database (Postgres)
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
@ -16,7 +16,7 @@ services:
|
|||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pg_isready -U postgres -d postgres"]
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@ -45,7 +45,7 @@ services:
|
|||||||
|
|
||||||
# Redis (for cache & server session management)
|
# Redis (for cache & server session management)
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --requirepass password
|
command: redis-server --requirepass password
|
||||||
|
|
||||||
@ -69,8 +69,8 @@ services:
|
|||||||
STORAGE_URL: http://localhost:9000
|
STORAGE_URL: http://localhost:9000
|
||||||
|
|
||||||
# -- Printer (Chrome) --
|
# -- Printer (Chrome) --
|
||||||
CHROME_URL: ws://chrome:3000
|
|
||||||
CHROME_TOKEN: chrome_token
|
CHROME_TOKEN: chrome_token
|
||||||
|
CHROME_URL: ws://chrome:3000
|
||||||
|
|
||||||
# -- Database (Postgres) --
|
# -- Database (Postgres) --
|
||||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||||
|
|||||||
@ -8,7 +8,7 @@ version: "3"
|
|||||||
services:
|
services:
|
||||||
# Database (Postgres)
|
# Database (Postgres)
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
@ -17,7 +17,7 @@ services:
|
|||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pg_isready -U postgres -d postgres"]
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@ -33,11 +33,11 @@ services:
|
|||||||
MINIO_ROOT_USER: minioadmin
|
MINIO_ROOT_USER: minioadmin
|
||||||
MINIO_ROOT_PASSWORD: minioadmin
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable
|
- traefik.enable=true
|
||||||
- traefik.http.routers.app.rule=Host(`storage.example.com`)
|
- traefik.http.routers.storage.rule=Host(`storage.example.com`)
|
||||||
- traefik.http.routers.app.entrypoints=websecure
|
- traefik.http.routers.storage.entrypoints=websecure
|
||||||
- traefik.http.routers.app.tls.certresolver=letsencrypt
|
- traefik.http.routers.storage.tls.certresolver=letsencrypt
|
||||||
- traefik.http.services.app.loadbalancer.server.port=9000
|
- traefik.http.services.storage.loadbalancer.server.port=9000
|
||||||
|
|
||||||
# Chrome Browser (for printing and previews)
|
# Chrome Browser (for printing and previews)
|
||||||
chrome:
|
chrome:
|
||||||
@ -47,10 +47,16 @@ services:
|
|||||||
TOKEN: chrome_token
|
TOKEN: chrome_token
|
||||||
EXIT_ON_HEALTH_FAILURE: true
|
EXIT_ON_HEALTH_FAILURE: true
|
||||||
PRE_REQUEST_HEALTH_CHECK: true
|
PRE_REQUEST_HEALTH_CHECK: true
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.printer.rule=Host(`printer.example.com`)
|
||||||
|
- traefik.http.routers.printer.entrypoints=websecure
|
||||||
|
- traefik.http.routers.printer.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.services.printer.loadbalancer.server.port=3000
|
||||||
|
|
||||||
# Redis (for cache & server session management)
|
# Redis (for cache & server session management)
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --save 60 1 --loglevel warning --requirepass password
|
command: redis-server --save 60 1 --loglevel warning --requirepass password
|
||||||
volumes:
|
volumes:
|
||||||
@ -70,12 +76,12 @@ services:
|
|||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
|
||||||
# -- URLs --
|
# -- URLs --
|
||||||
PUBLIC_URL: http://example.com
|
PUBLIC_URL: https://example.com
|
||||||
STORAGE_URL: http://storage.example.com
|
STORAGE_URL: https://storage.example.com
|
||||||
|
|
||||||
# -- Printer (Chrome) --
|
# -- Printer (Chrome) --
|
||||||
CHROME_URL: ws://chrome:3000
|
|
||||||
CHROME_TOKEN: chrome_token
|
CHROME_TOKEN: chrome_token
|
||||||
|
CHROME_URL: wss://printer.example.com
|
||||||
|
|
||||||
# -- Database (Postgres) --
|
# -- Database (Postgres) --
|
||||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||||
@ -111,22 +117,21 @@ services:
|
|||||||
GOOGLE_CLIENT_SECRET: google_client_secret
|
GOOGLE_CLIENT_SECRET: google_client_secret
|
||||||
GOOGLE_CALLBACK_URL: http://localhost:3000/api/auth/google/callback
|
GOOGLE_CALLBACK_URL: http://localhost:3000/api/auth/google/callback
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable
|
- traefik.enable=true
|
||||||
- traefik.http.routers.app.rule=Host(`example.com`)
|
- traefik.http.routers.app.rule=Host(`example.com`)
|
||||||
- traefik.http.routers.app.entrypoints=websecure
|
- traefik.http.routers.app.entrypoints=websecure
|
||||||
- traefik.http.routers.app.tls
|
|
||||||
- traefik.http.routers.app.tls.certresolver=letsencrypt
|
- traefik.http.routers.app.tls.certresolver=letsencrypt
|
||||||
- traefik.http.services.app.loadbalancer.server.port=3000
|
- traefik.http.services.app.loadbalancer.server.port=3000
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik
|
image: traefik
|
||||||
command:
|
command:
|
||||||
- --api
|
- --api=true
|
||||||
- --providers.docker
|
- --providers.docker=true
|
||||||
- --providers.docker.exposedbydefault=false
|
- --providers.docker.exposedbydefault=false
|
||||||
- --entrypoints.web.address=:80
|
- --entrypoints.web.address=:80
|
||||||
- --entrypoints.websecure.address=:443
|
- --entrypoints.websecure.address=:443
|
||||||
- --certificatesresolvers.letsencrypt.acme.tlschallenge
|
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
|
||||||
- --certificatesresolvers.letsencrypt.acme.email=noreply@example.com
|
- --certificatesresolvers.letsencrypt.acme.email=noreply@example.com
|
||||||
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ version: "3"
|
|||||||
services:
|
services:
|
||||||
# Database (Postgres)
|
# Database (Postgres)
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
@ -17,7 +17,7 @@ services:
|
|||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pg_isready -U postgres -d postgres"]
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@ -34,8 +34,8 @@ services:
|
|||||||
MINIO_ROOT_PASSWORD: minioadmin
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.minio.rule=Host(`storage.example.com`)
|
- traefik.http.routers.storage.rule=Host(`storage.example.com`)
|
||||||
- traefik.http.services.minio.loadbalancer.server.port=9000
|
- traefik.http.services.storage.loadbalancer.server.port=9000
|
||||||
|
|
||||||
# Chrome Browser (for printing and previews)
|
# Chrome Browser (for printing and previews)
|
||||||
chrome:
|
chrome:
|
||||||
@ -45,10 +45,14 @@ services:
|
|||||||
TOKEN: chrome_token
|
TOKEN: chrome_token
|
||||||
EXIT_ON_HEALTH_FAILURE: true
|
EXIT_ON_HEALTH_FAILURE: true
|
||||||
PRE_REQUEST_HEALTH_CHECK: true
|
PRE_REQUEST_HEALTH_CHECK: true
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.printer.rule=Host(`printer.example.com`)
|
||||||
|
- traefik.http.services.printer.loadbalancer.server.port=3000
|
||||||
|
|
||||||
# Redis (for cache & server session management)
|
# Redis (for cache & server session management)
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --requirepass password
|
command: redis-server --requirepass password
|
||||||
|
|
||||||
@ -70,8 +74,8 @@ services:
|
|||||||
STORAGE_URL: http://storage.example.com
|
STORAGE_URL: http://storage.example.com
|
||||||
|
|
||||||
# -- Printer (Chrome) --
|
# -- Printer (Chrome) --
|
||||||
CHROME_URL: ws://chrome:3000
|
|
||||||
CHROME_TOKEN: chrome_token
|
CHROME_TOKEN: chrome_token
|
||||||
|
CHROME_URL: ws://chrome:3000
|
||||||
|
|
||||||
# -- Database (Postgres) --
|
# -- Database (Postgres) --
|
||||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||||
@ -115,7 +119,7 @@ services:
|
|||||||
image: traefik
|
image: traefik
|
||||||
command:
|
command:
|
||||||
- --api.insecure=true
|
- --api.insecure=true
|
||||||
- --providers.docker
|
- --providers.docker=true
|
||||||
- --providers.docker.exposedbydefault=false
|
- --providers.docker.exposedbydefault=false
|
||||||
- --entrypoints.web.address=:80
|
- --entrypoints.web.address=:80
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
Reference in New Issue
Block a user