coolify haproxy 集成简单说明
coolify 以前介绍过,是一个开源heroku 以及netlify的替换方案,对于服务的访问层集成了haproxy 进行处理
细节上使用了haproxy 的dataplaneapi 进行处理
api 调用部分
核心是利用了模版引擎mustache 以及直接调用的dataplaneapi
- 初始化部分
主要是获取数据库数据,进行初始化
import { dev } from '$app/env';
import got from 'got';
import mustache from 'mustache';
import crypto from 'crypto';
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
let template = `program api
command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi
no option start-on-reload
global
stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
log stdout format raw local0 debug
defaults
mode http
log global
timeout http-request 60s
timeout connect 10s
timeout client 60s
timeout server 60s
userlist haproxy-dataplaneapi
user admin insecure-password "\${HAPROXY_PASSWORD}"
frontend http
mode http
bind :80
bind :443 ssl crt /usr/local/etc/haproxy/ssl/ alpn h2,http/1.1
acl is_certbot path_beg /.well-known/acme-challenge/
{{#applications}}
{{#isHttps}}
http-request redirect scheme https code ${
dev ? 302 : 301
} if { hdr(host) -i {{domain}} } !{ ssl_fc }
{{/isHttps}}
http-request redirect location {{{redirectValue}}} code ${
dev ? 302 : 301
} if { req.hdr(host) -i {{redirectTo}} }
{{/applications}}
{{#services}}
{{#isHttps}}
http-request redirect scheme https code ${
dev ? 302 : 301
} if { hdr(host) -i {{domain}} } !{ ssl_fc }
{{/isHttps}}
http-request redirect location {{{redirectValue}}} code ${
dev ? 302 : 301
} if { req.hdr(host) -i {{redirectTo}} }
{{/services}}
{{#coolify}}
{{#isHttps}}
http-request redirect scheme https code ${
dev ? 302 : 301
} if { hdr(host) -i {{domain}} } !{ ssl_fc }
{{/isHttps}}
http-request redirect location {{{redirectValue}}} code ${
dev ? 302 : 301
} if { req.hdr(host) -i {{redirectTo}} }
{{/coolify}}
use_backend backend-certbot if is_certbot
use_backend %[req.hdr(host),lower]
frontend stats
bind *:8404
stats enable
stats uri /
stats admin if TRUE
stats auth "\${HAPROXY_USERNAME}:\${HAPROXY_PASSWORD}"
backend backend-certbot
mode http
server certbot host.docker.internal:9080
{{#applications}}
{{#isRunning}}
# updatedAt={{updatedAt}}
backend {{domain}}
option forwardfor
{{#isHttps}}
http-request add-header X-Forwarded-Proto https
{{/isHttps}}
{{^isHttps}}
http-request add-header X-Forwarded-Proto http
{{/isHttps}}
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
server {{id}} {{id}}:{{port}}
{{/isRunning}}
{{/applications}}
{{#services}}
{{#isRunning}}
# updatedAt={{updatedAt}}
backend {{domain}}
option forwardfor
{{#isHttps}}
http-request add-header X-Forwarded-Proto https
{{/isHttps}}
{{^isHttps}}
http-request add-header X-Forwarded-Proto http
{{/isHttps}}
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
server {{id}} {{id}}:{{port}}
{{/isRunning}}
{{/services}}
{{#coolify}}
backend {{domain}}
option forwardfor
option httpchk GET /undead.json
{{#isHttps}}
http-request add-header X-Forwarded-Proto https
{{/isHttps}}
{{^isHttps}}
http-request add-header X-Forwarded-Proto http
{{/isHttps}}
http-request add-header X-Forwarded-Host %[req.hdr(host),lower]
server {{id}} {{id}}:{{port}} check fall 10
{{/coolify}}
`;
export async function haproxyInstance() {
const { proxyPassword } = await db.listSettings();
return got.extend({
prefixUrl: url,
username: 'admin',
password: proxyPassword
});
}
export async function configureHAProxy() {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
try {
const data = {
applications: [],
services: [],
coolify: []
};
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }
});
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews },
updatedAt
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.applications.push({
id,
port: port || 3000,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
data.applications.push({
id: container,
port: port || 3000,
domain: previewDomain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true
}
});
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
}
});
}
} catch (error) {
throw error;
}
}
- dataplaneapi 调用
难度不大, 就是调用api,对于api 调用部分,我以前也写过文章可以参考
import { dev } from '$app/env';
import { asyncExecShell, getEngine } from '$lib/common';
import got from 'got';
import * as db from '$lib/database';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
export async function haproxyInstance() {
const { proxyPassword } = await db.listSettings();
return got.extend({
prefixUrl: url,
username: 'admin',
password: proxyPassword
});
}
export async function getRawConfiguration(): Promise<RawHaproxyConfiguration> {
return await (await haproxyInstance()).get(`v2/services/haproxy/configuration/raw`).json();
}
export async function getNextTransactionVersion(): Promise<number> {
const raw = await getRawConfiguration();
if (raw?._version) {
return raw._version;
}
return 1;
}
export async function getNextTransactionId(): Promise<string> {
const version = await getNextTransactionVersion();
const newTransaction: NewTransaction = await (
await haproxyInstance()
)
.post('v2/services/haproxy/transactions', {
searchParams: {
version
}
})
.json();
return newTransaction.id;
}
export async function completeTransaction(transactionId) {
const haproxy = await haproxyInstance();
return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
}
export async function deleteProxy({ id }) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
await haproxy.get(`v2/services/haproxy/configuration/backends/${id}`).json();
transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/backends/${id}`, {
searchParams: {
transaction_id: transactionId
}
})
.json();
await haproxy.get(`v2/services/haproxy/configuration/frontends/${id}`).json();
await haproxy
.delete(`v2/services/haproxy/configuration/frontends/${id}`, {
searchParams: {
transaction_id: transactionId
}
})
.json();
} catch (error) {
console.log(error.response?.body || error);
} finally {
if (transactionId) await completeTransaction(transactionId);
}
}
export async function reloadHaproxy(engine) {
const host = getEngine(engine);
return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`);
}
export async function checkHAProxy(haproxy?: any) {
if (!haproxy) haproxy = await haproxyInstance();
try {
await haproxy.get('v2/info');
} catch (error) {
throw {
message:
'Coolify Proxy is not running, but it should be!
Start it in the "Destinations" menu.'
};
}
}
export async function stopTcpHttpProxy(destinationDocker, publicPort) {
const { engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName);
try {
if (found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) {
return error;
}
}
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id);
try {
if (foundDB && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
const ip = JSON.parse(Config)[0].Gateway;
return await asyncExecShell(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${
volume ? `-v ${volume}` : ''
} -d coollabsio/${defaultProxyImageTcp}`
);
}
} catch (error) {
return error;
}
}
export async function startHttpProxy(destinationDocker, id, publicPort, privatePort) {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id);
try {
if (foundDB && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
const ip = JSON.parse(Config)[0].Gateway;
return await asyncExecShell(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageHttp}`
);
}
} catch (error) {
return error;
}
}
export async function startCoolifyProxy(engine) {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy');
const { proxyPassword, proxyUser, id } = await db.listSettings();
if (!found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
const ip = JSON.parse(Config)[0].Gateway;
await asyncExecShell(
`DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}`
);
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
}
await configureNetworkCoolifyProxy(engine);
}
export async function checkContainer(engine, container) {
const host = getEngine(engine);
let containerFound = false;
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}`
);
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running' ? true : false;
if (status === 'exited' || status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
if (isRunning) {
containerFound = true;
}
} catch (err) {
// Container not found
}
return containerFound;
}
export async function stopCoolifyProxy(engine) {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy');
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false });
const { id } = await db.prisma.setting.findFirst({});
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
try {
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
);
}
} catch (error) {
return error;
}
}
export async function configureNetworkCoolifyProxy(engine) {
const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
destinations.forEach(async (destination) => {
try {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
);
} catch (err) {
// TODO: handle error
}
});
}
说明
实际上官方的处理部分并不是很好,比如获取事务版本的,是直接读取整个配置,实际上有更好的获取版本api
参考资料
https://github.com/haproxytech/dataplaneapi
https://www.haproxy.com/documentation/dataplaneapi/