WIP: Traefik

This commit is contained in:
Andras Bacsai
2022-05-16 16:11:35 +02:00
parent 1fa5c5e021
commit 4f4f5b1c01
6 changed files with 253 additions and 110 deletions

View File

@@ -39,3 +39,5 @@ volumes:
name: coolify-ssl-certs name: coolify-ssl-certs
coolify-letsencrypt: coolify-letsencrypt:
name: coolify-letsencrypt name: coolify-letsencrypt
coolify-traefik-letsencrypt:
name: coolify-traefik-letsencrypt

View File

@@ -156,7 +156,7 @@ export async function startTraefikTCPProxy(
`--entrypoints.tcp.address=:${publicPort}`, `--entrypoints.tcp.address=:${publicPort}`,
`--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp`, `--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp`,
'--providers.http.pollTimeout=2s', '--providers.http.pollTimeout=2s',
'--log.level=debug' '--log.level=error'
], ],
ports: [`${publicPort}:${publicPort}`], ports: [`${publicPort}:${publicPort}`],
extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`],
@@ -252,7 +252,7 @@ export async function startTraefikHTTPProxy(
`--entrypoints.http.address=:${publicPort}`, `--entrypoints.http.address=:${publicPort}`,
`--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`, `--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`,
'--providers.http.pollTimeout=2s', '--providers.http.pollTimeout=2s',
'--log.level=debug' '--log.level=error'
], ],
ports: [`${publicPort}:${publicPort}`], ports: [`${publicPort}:${publicPort}`],
extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`],
@@ -343,7 +343,28 @@ export async function startTraefikProxy(engine: string): Promise<void> {
); );
const ip = JSON.parse(Config)[0].Gateway; const ip = JSON.parse(Config)[0].Gateway;
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST="${host}" docker run --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl -v /var/run/docker.sock:/var/run/docker.sock --network coolify-infra -p "80:80" -p "443:443" -p "8080:8080" --name coolify-proxy -d ${defaultTraefikImage} --entrypoints.web.address=:80 --entrypoints.websecure.address=:443 --providers.docker=true --providers.docker.exposedbydefault=false --providers.http.endpoint=${coolifyEndpoint} --providers.http.pollTimeout=5s --log.level=error` `DOCKER_HOST="${host}" docker run --restart always \
--add-host 'host.docker.internal:host-gateway' \
--add-host 'host.docker.internal:${ip}' \
-v coolify-traefik-letsencrypt:/etc/traefik/acme \
-v /var/run/docker.sock:/var/run/docker.sock \
--network coolify-infra \
-p "80:80" \
-p "443:443" \
-p "8080:8080" \
--name coolify-proxy \
-d ${defaultTraefikImage} \
--api.insecure=true \
--entrypoints.web.address=:80 \
--entrypoints.websecure.address=:443 \
--providers.docker=true \
--providers.docker.exposedbydefault=false \
--providers.http.endpoint=${coolifyEndpoint} \
--providers.http.pollTimeout=5s \
--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
--log.level=debug`
); );
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true });

View File

@@ -1,12 +1,15 @@
import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database'; import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database';
import { import {
checkContainer,
startCoolifyProxy, startCoolifyProxy,
startHttpProxy, startHttpProxy,
startTcpProxy, startTcpProxy,
startTraefikHTTPProxy, startTraefikHTTPProxy,
startTraefikProxy, startTraefikProxy,
startTraefikTCPProxy, startTraefikTCPProxy,
stopTcpHttpProxy stopCoolifyProxy,
stopTcpHttpProxy,
stopTraefikProxy
} from '$lib/haproxy'; } from '$lib/haproxy';
export default async function (): Promise<void | { export default async function (): Promise<void | {
@@ -14,16 +17,21 @@ export default async function (): Promise<void | {
body: { message: string; error: string }; body: { message: string; error: string };
}> { }> {
try { try {
const settings = await prisma.setting.findFirst();
// Coolify Proxy // Coolify Proxy
const engine = '/var/run/docker.sock';
const settings = await prisma.setting.findFirst();
const localDocker = await prisma.destinationDocker.findFirst({ const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' } where: { engine }
}); });
if (localDocker && localDocker.isCoolifyProxyUsed) { if (localDocker && localDocker.isCoolifyProxyUsed) {
if (settings.isTraefikUsed) { if (settings.isTraefikUsed) {
await startTraefikProxy('/var/run/docker.sock'); const found = await checkContainer(engine, 'coolify-haproxy');
if (found) await stopCoolifyProxy(engine);
await startTraefikProxy(engine);
} else { } else {
await startCoolifyProxy('/var/run/docker.sock'); const found = await checkContainer(engine, 'coolify-proxy');
if (found) await stopTraefikProxy(engine);
await startCoolifyProxy(engine);
} }
} }

View File

@@ -1,8 +1,12 @@
import { generateSSLCerts } from '$lib/letsencrypt'; import { generateSSLCerts } from '$lib/letsencrypt';
import { prisma } from '$lib/database';
export default async function (): Promise<void> { export default async function (): Promise<void> {
try { try {
const settings = await prisma.setting.findFirst();
if (!settings.isTraefikUsed) {
return await generateSSLCerts(); return await generateSSLCerts();
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
throw error; throw error;

View File

@@ -1,8 +1,12 @@
import { renewSSLCerts } from '$lib/letsencrypt'; import { renewSSLCerts } from '$lib/letsencrypt';
import { prisma } from '$lib/database';
export default async function (): Promise<void> { export default async function (): Promise<void> {
try { try {
const settings = await prisma.setting.findFirst();
if (!settings.isTraefikUsed) {
return await renewSSLCerts(); return await renewSSLCerts();
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
throw error; throw error;

View File

@@ -6,16 +6,49 @@ import { listServicesWithIncludes } from '$lib/database';
import { checkContainer } from '$lib/haproxy'; import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
function generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik }) {
if (!isDualCerts) {
if (isWWW) {
if (isHttps) {
traefik.http.routers[id].middlewares?.length > 0
? traefik.http.routers[id].middlewares.push('https-redirect-non-www-to-www')
: (traefik.http.routers[id].middlewares = [
'https-redirect-non-www-to-www',
'http-to-https'
]);
} else {
traefik.http.routers[id].middlewares?.length > 0
? traefik.http.routers[id].middlewares.push('http-redirect-non-www-to-www')
: (traefik.http.routers[id].middlewares = [
'http-redirect-non-www-to-www',
'https-to-http'
]);
}
} else {
if (isHttps) {
traefik.http.routers[id].middlewares?.length > 0
? traefik.http.routers[id].middlewares.push('https-redirect-www-to-non-www')
: (traefik.http.routers[id].middlewares = [
'https-redirect-www-to-non-www',
'http-to-https'
]);
} else {
traefik.http.routers[id]?.middlewares?.length > 0
? traefik.http.routers[id].middlewares.push('http-redirect-www-to-non-www')
: (traefik.http.routers[id].middlewares = ['http-redirect-www-to-non-www']);
}
}
}
}
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const id = event.url.searchParams.get('id'); const id = event.url.searchParams.get('id');
if (id) { if (id) {
const privatePort = event.url.searchParams.get('privatePort'); const privatePort = event.url.searchParams.get('privatePort');
const publicPort = event.url.searchParams.get('publicPort'); const publicPort = event.url.searchParams.get('publicPort');
const type = event.url.searchParams.get('type'); const type = event.url.searchParams.get('type');
let traefik = {};
if (publicPort) { if (publicPort) {
if (type === 'tcp') { if (type === 'tcp') {
traefik = { const traefik = {
[type]: { [type]: {
routers: { routers: {
[id]: { [id]: {
@@ -27,48 +60,65 @@ export const get: RequestHandler = async (event) => {
services: { services: {
[id]: { [id]: {
loadbalancer: { loadbalancer: {
servers: [] servers: [{ address: `${id}:${privatePort}` }]
}
}
},
middlewares: {
['global-compress']: {
compress: true
} }
} }
} }
};
return {
status: 200,
body: {
...traefik
} }
}; };
} else if (type === 'http') { } else if (type === 'http') {
const service = await db.prisma.service.findFirst({ where: { id } }); const service = await db.prisma.service.findFirst({ where: { id } });
if (service?.fqdn) { if (service?.fqdn) {
const domain = getDomain(service.fqdn); const domain = getDomain(service.fqdn);
traefik = { const isWWW = domain.startsWith('www.');
const traefik = {
[type]: { [type]: {
routers: { routers: {
[id]: { [id]: {
entrypoints: [type], entrypoints: [type],
rule: `Host(\`${domain}\`)`, rule: isWWW
? `Host(\`${domain}\`) || Host(\`www.${domain}\`)`
: `Host(\`${domain}\`)`,
service: id service: id
} }
}, },
services: { services: {
[id]: { [id]: {
loadbalancer: { loadbalancer: {
servers: [] servers: [{ url: `http://${id}:${privatePort}` }]
} }
} }
},
middlewares: {
['global-compress']: {
compress: true
}
} }
} }
}; };
}
}
}
if (type === 'tcp') {
traefik[type].services[id].loadbalancer.servers.push({ address: `${id}:${privatePort}` });
} else if (type === 'http') {
traefik[type].services[id].loadbalancer.servers.push({ url: `http://${id}:${privatePort}` });
}
return { return {
status: 200, status: 200,
body: { body: {
...traefik ...traefik
} }
}; };
}
}
}
return {
status: 500
};
} else { } else {
const applications = await db.prisma.application.findMany({ const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }
@@ -85,27 +135,26 @@ export const get: RequestHandler = async (event) => {
port, port,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings: { previews }, settings: { previews, dualCerts }
updatedAt
} = application; } = application;
if (destinationDockerId) { if (destinationDockerId) {
const { engine, network } = destinationDocker; const { engine, network } = destinationDocker;
const isRunning = await checkContainer(engine, id); const isRunning = await checkContainer(engine, id);
if (fqdn) { if (fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) { if (isRunning) {
data.applications.push({ data.applications.push({
id, id,
port: port || 3000, port: port || 3000,
domain, domain,
nakedDomain,
isRunning, isRunning,
isHttps, isHttps,
redirectValue, isWWW,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, isDualCerts: dualCerts
updatedAt: updatedAt.getTime()
}); });
} }
if (previews) { if (previews) {
@@ -127,9 +176,7 @@ export const get: RequestHandler = async (event) => {
domain: previewDomain, domain: previewDomain,
isRunning, isRunning,
isHttps, isHttps,
redirectValue, isWWW
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
}); });
} }
} }
@@ -144,9 +191,9 @@ export const get: RequestHandler = async (event) => {
fqdn, fqdn,
id, id,
type, type,
dualCerts,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
updatedAt,
plausibleAnalytics plausibleAnalytics
} = service; } = service;
if (destinationDockerId) { if (destinationDockerId) {
@@ -158,9 +205,9 @@ export const get: RequestHandler = async (event) => {
const isRunning = await checkContainer(engine, id); const isRunning = await checkContainer(engine, id);
if (fqdn) { if (fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) { if (isRunning) {
// Plausible Analytics custom script // Plausible Analytics custom script
let scriptName = false; let scriptName = false;
@@ -170,17 +217,16 @@ export const get: RequestHandler = async (event) => {
) { ) {
scriptName = plausibleAnalytics.scriptName; scriptName = plausibleAnalytics.scriptName;
} }
data.services.push({ data.services.push({
id, id,
port, port,
publicPort, publicPort,
domain, domain,
nakedDomain,
isRunning, isRunning,
isHttps, isHttps,
redirectValue, isWWW,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, isDualCerts: dualCerts,
updatedAt: updatedAt.getTime(),
scriptName scriptName
}); });
} }
@@ -189,34 +235,115 @@ export const get: RequestHandler = async (event) => {
} }
} }
const { fqdn } = await db.prisma.setting.findFirst(); const { fqdn, dualCerts } = await db.prisma.setting.findFirst();
if (fqdn) { if (fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({ data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify', id: dev ? 'host.docker.internal' : 'coolify',
port: 3000, port: 3000,
domain, domain,
nakedDomain,
isHttps, isHttps,
redirectValue, isWWW,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain isDualCerts: dualCerts
}); });
} }
const traefik = { const traefik = {
http: { http: {
routers: {}, routers: {},
services: {} services: {},
middlewares: {
['global-compress']: {
compress: true
},
['https-redirect-non-www-to-www']: {
redirectregex: {
regex: '^https://(?:www\\.)?(.+)',
replacement: 'https://www.${1}',
permanent: dev ? false : true
}
},
['http-redirect-non-www-to-www']: {
redirectregex: {
regex: '^http://(?:www\\.)?(.+)',
replacement: 'http://www.${1}',
permanent: dev ? false : true
}
},
['https-redirect-www-to-non-www']: {
redirectregex: {
regex: '^https?://www\\.(.+)',
replacement: 'https://${1}',
permanent: dev ? false : true
}
},
['http-redirect-www-to-non-www']: {
redirectregex: {
regex: '^http?://www\\.(.+)',
replacement: 'http://${1}',
permanent: dev ? false : true
}
},
['http-to-https']: {
redirectregex: {
regex: '^http?://(.+)',
replacement: 'https://${1}',
permanent: dev ? false : true
}
},
['https-to-http']: {
redirectregex: {
regex: '^https?://(.+)',
replacement: 'http://${1}',
permanent: dev ? false : true
}
},
['https-http']: {
redirectscheme: {
scheme: 'http',
permanent: false
}
}
}
} }
}; };
for (const application of data.applications) { for (const application of data.applications) {
const { id, port, domain, isHttps, redirectValue, redirectTo, updatedAt } = application; const { id, port, domain, nakedDomain, isHttps, isWWW, isDualCerts } = application;
if (isHttps) {
traefik.http.routers[id] = { traefik.http.routers[id] = {
entrypoints: ['web'], entrypoints: ['web'],
rule: `Host(\`${domain}\`)`, rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
middlewares: ['http-to-https'],
service: id service: id
}; };
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: isWWW
? isDualCerts
? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`
: `Host(\`${nakedDomain}\`)`
: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: id
};
} else {
traefik.http.routers[id] = {
entrypoints: ['web'],
rule: isWWW
? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`
: `Host(\`${nakedDomain}\`)`,
service: id
};
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
middlewares: ['https-http'],
service: id
};
}
traefik.http.services[id] = { traefik.http.services[id] = {
loadbalancer: { loadbalancer: {
servers: [ servers: [
@@ -226,14 +353,23 @@ export const get: RequestHandler = async (event) => {
] ]
} }
}; };
if (isHttps && !dev) {
traefik.http.routers[id].tls = {
certresolver: 'letsencrypt'
};
} }
for (const application of data.services) { generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik });
const { id, port, domain, isHttps, redirectValue, redirectTo, updatedAt, scriptName } = }
application; for (const service of data.services) {
const { id, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName } = service;
traefik.http.routers[id] = { traefik.http.routers[id] = {
entrypoints: ['web'], entrypoints: isHttps ? ['web', 'websecure'] : ['web'],
rule: `Host(\`${domain}\`)`, rule: isWWW
? isDualCerts
? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`
: `Host(\`${nakedDomain}\`)`
: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: id service: id
}; };
traefik.http.services[id] = { traefik.http.services[id] = {
@@ -245,22 +381,33 @@ export const get: RequestHandler = async (event) => {
] ]
} }
}; };
if (isHttps && !dev) {
traefik.http.routers[id].tls = {
certresolver: 'letsencrypt'
};
}
if (scriptName) { if (scriptName) {
if (!traefik.http.middlewares) traefik.http.middlewares = {}; if (!traefik.http.middlewares) traefik.http.middlewares = {};
traefik.http.middlewares[`${id}-redir`] = { traefik.http.middlewares[`${id}-redir`] = {
replacepathregex: { replacepathregex: {
regex: `/js/${scriptName}`, regex: `/js/${scriptName}`,
replacement: '/js/plausible.js' replacement: '/js/plausible.js',
permanent: false
} }
}; };
traefik.http.routers[id].middlewares = [`${id}-redir`]; traefik.http.routers[id].middlewares = [`${id}-redir`];
} }
generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik });
} }
for (const application of data.coolify) { for (const coolify of data.coolify) {
const { domain, id, port } = application; const { nakedDomain, domain, id, port, isHttps, isWWW, isDualCerts } = coolify;
traefik.http.routers['coolify'] = { traefik.http.routers['coolify'] = {
entrypoints: ['web'], entrypoints: isHttps ? ['web', 'websecure'] : ['web'],
rule: `Host(\`${domain}\`)`, rule: isWWW
? isDualCerts
? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`
: `Host(\`${nakedDomain}\`)`
: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: id service: id
}; };
traefik.http.services[id] = { traefik.http.services[id] = {
@@ -272,61 +419,18 @@ export const get: RequestHandler = async (event) => {
] ]
} }
}; };
if (isHttps && !dev) {
traefik.http.routers[id].tls = {
certresolver: 'letsencrypt'
};
}
generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik });
} }
return { return {
status: 200, status: 200,
body: { body: {
...traefik ...traefik
// "http": {
// "routers": {
// "coolify": {
// "entrypoints": [
// "web"
// ],
// "middlewares": [
// "coolify-hc"
// ],
// "rule": "Host(`staging.coolify.io`)",
// "service": "coolify"
// },
// "static.example.coolify.io": {
// "entrypoints": [
// "web"
// ],
// "rule": "Host(`static.example.coolify.io`)",
// "service": "static.example.coolify.io"
// }
// },
// "services": {
// "coolify": {
// "loadbalancer": {
// "servers": [
// {
// "url": "http://coolify:3000"
// }
// ]
// }
// },
// "static.example.coolify.io": {
// "loadbalancer": {
// "servers": [
// {
// "url": "http://cl32p06f58068518cs3thg6vbc7:80"
// }
// ]
// }
// }
// },
// "middlewares": {
// "coolify-hc": {
// "replacepathregex": {
// "regex": "/dead.json",
// "replacement": "/undead.json"
// }
// }
// }
// }
} }
}; };
} }