Feature/bouncer ()

* Initial work

* Setup script stub

* Wrap runit and call dos2unix beforehand.

* Working to the point I need to make it sync in and out of s3.

* Seems like we're done and its working.

* Add build process.

* Add build process.

* Bugfixes discovered during deployment.

* Copy certs into /live because certbot is a pain.

* More elegant about hammering letsencrypt.

* Working!
This commit is contained in:
Greyscale 2021-06-06 17:38:46 +02:00 committed by GitHub
parent d820562de7
commit 2fd5c62074
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 4086 additions and 2 deletions

34
.github/workflows/bouncer.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Build Nginx + LetsEncrypt Bouncer
on:
push:
paths:
- bouncer
workflow_run:
workflows:
- Build PHP Flavours
branches: [ 'master', 'feature/**' ]
types:
- completed
jobs:
bouncer-build:
name: "Bake Bouncer Container"
runs-on: self-hosted
steps:
- uses: actions/checkout@v1
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
name: Login to Docker Hub
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- uses: docker/build-push-action@v2
name: Build & Push
with:
context: bouncer
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: benzine/bouncer

1
bouncer/.dockerignore Normal file
View file

@ -0,0 +1 @@
vendor

3
bouncer/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/docker-compose.override.yml
/vendor
/.php-cs-fixer.cache

22
bouncer/.php-cs-fixer.php Normal file
View file

@ -0,0 +1,22 @@
<?php
$finder = PhpCsFixer\Finder::create();
$finder->in(__DIR__);
return (new PhpCsFixer\Config)
->setRiskyAllowed(true)
->setHideProgress(false)
->setRules([
'@PSR2' => true,
'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
'@PhpCsFixer' => true,
'@PHP73Migration' => true,
'no_php4_constructor' => true,
'no_unused_imports' => true,
'no_useless_else' => true,
'no_superfluous_phpdoc_tags' => true,
'void_return' => true,
'yoda_style' => false,
])
->setFinder($finder)
;

48
bouncer/Dockerfile Normal file
View file

@ -0,0 +1,48 @@
FROM benzine/php:cli-8.0
LABEL maintainer="Matthew Baggett <matthew@baggett.me>" \
org.label-schema.vcs-url="https://github.com/benzine-framework/docker"
COPY self-signed-certificates /certs
# Install nginx, certbot
RUN apt-get -qq update && \
# Install pre-dependencies to use apt-key.
apt-get -yqq install --no-install-recommends \
lsb-core \
gnupg \
&& \
# Add nginx ppa
sh -c 'echo "deb http://ppa.launchpad.net/nginx/stable/ubuntu $(lsb_release -sc) main" \
> /etc/apt/sources.list.d/nginx-stable.list' && \
# Add nginx key
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C && \
apt-get -qq update && \
apt-get -yqq install --no-install-recommends \
nginx \
python-certbot-nginx \
&& \
apt-get remove -yqq \
lsb-core \
cups-common \
&& \
apt-get autoremove -yqq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/lib/dpkg/status.old /var/cache/debconf/templates.dat /var/log/dpkg.log /var/log/lastlog /var/log/apt/*.log
VOLUME /etc/letsencrypt
COPY nginx.runit /etc/service/nginx/run
COPY logs.runit /etc/service/nginx-logs/run
COPY bouncer.runit /etc/service/bouncer/run
COPY logs-nginx-access.runit /etc/service/logs-nginx-access/run
COPY logs-nginx-error.runit /etc/service/logs-nginx-error/run
RUN chmod +x /etc/service/*/run
COPY NginxDefault /etc/nginx/sites-enabled/default
COPY NginxSSL /etc/nginx/sites-enabled/default-ssl
COPY NginxTemplate.twig /app/
# Disable daemonising in nginx
RUN sed -i '1s;^;daemon off\;\n;' /etc/nginx/nginx.conf
COPY bouncer /app
COPY composer.* /app/
RUN composer install && \
chmod +x /app/bouncer && \
mkdir -p /var/log/bouncer

25
bouncer/NginxDefault Normal file
View file

@ -0,0 +1,25 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
client_max_body_size 1024M;
root /app/public;
server_name _;
index index.html index.htm;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
location ~ /\.ht {
deny all;
}
}

30
bouncer/NginxSSL Normal file
View file

@ -0,0 +1,30 @@
server {
listen 443 ssl;
listen [::]:443 ssl;
client_max_body_size 1024M;
root /app/public;
server_name _;
index index.html index.htm;
ssl_certificate /certs/example.crt;
ssl_certificate_key /certs/example.key;
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# ssl_ciphers HIGH:!aNULL:!MD5;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
location ~ /\.ht {
deny all;
}
}

View file

@ -0,0 +1,33 @@
server {
{% if allowNonSSL %}
listen 80;
listen [::]:80;
{% endif %}
listen 443 ssl;
listen [::]:443 ssl;
server_name {{ domains|join(' ') }};
access_log /var/log/bouncer/{{ name }}.access.log;
error_log /var/log/bouncer/{{ name }}.error.log;
{% if useTemporaryCert %}
ssl_certificate /certs/example.crt;
ssl_certificate_key /certs/example.key;
{% else %}
ssl_certificate /etc/letsencrypt/live/{{ name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ name }}/privkey.pem;
{% endif %}
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass {{ targetPath }};
}
}
{% if not allowNonSSL %}
server {
listen 80;
listen [::]:80;
server_name {{ domains|join(' ') }};
return 301 https://$host$request_uri;
}
{% endif %}

488
bouncer/bouncer Executable file
View file

@ -0,0 +1,488 @@
#!/usr/bin/env php
<?php
require_once 'vendor/autoload.php';
use AdamBrett\ShellWrapper\Command\Builder as CommandBuilder;
use AdamBrett\ShellWrapper\Runners\Exec;
use Aws\S3\S3Client;
use Bramus\Monolog\Formatter\ColoredLineFormatter;
use GuzzleHttp\Client as Guzzle;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use League\Flysystem\FileAttributes;
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Spatie\Emoji\Emoji;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
class BouncerTarget
{
private string $id;
private array $domains;
private string $ip;
private int $port = 80;
private bool $letsEncrypt = false;
private string $targetPath;
private bool $allowNonSSL = false;
private bool $useTemporaryCert = true;
public function __toArray()
{
return [
'id' => $this->getId(),
'name' => $this->getName(),
'domains' => $this->getDomains(),
'letsEncrypt' => $this->isLetsEncrypt(),
'targetPath' => $this->getTargetPath(),
'useTemporaryCert' => $this->isUseTemporaryCert(),
'allowNonSSL' => $this->isAllowNonSSL(),
];
}
public function isUseTemporaryCert(): bool
{
return $this->useTemporaryCert;
}
public function setUseTemporaryCert(bool $useTemporaryCert): BouncerTarget
{
$this->useTemporaryCert = $useTemporaryCert;
return $this;
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): BouncerTarget
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getDomains(): array
{
return $this->domains;
}
/**
* @param string $domains
*/
public function setDomains(array $domains): BouncerTarget
{
$this->domains = $domains;
return $this;
}
public function isLetsEncrypt(): bool
{
return $this->letsEncrypt;
}
public function setLetsEncrypt(bool $letsEncrypt): BouncerTarget
{
$this->letsEncrypt = $letsEncrypt;
return $this;
}
public function getTargetPath(): string
{
return $this->targetPath;
}
public function setTargetPath(string $targetPath): BouncerTarget
{
$this->targetPath = $targetPath;
return $this;
}
public function getIp(): string
{
return $this->ip;
}
public function setIp(string $ip): BouncerTarget
{
$this->ip = $ip;
return $this;
}
public function getPort(): int
{
return $this->port;
}
public function setPort(int $port): BouncerTarget
{
$this->port = $port;
return $this;
}
public function getName()
{
return reset($this->domains);
}
public function isAllowNonSSL(): bool
{
return $this->allowNonSSL;
}
public function setAllowNonSSL(bool $allowNonSSL): BouncerTarget
{
$this->allowNonSSL = $allowNonSSL;
return $this;
}
}
class Bouncer
{
private array $environment;
private Guzzle $client;
private FilesystemLoader $loader;
private Environment $twig;
private Filesystem $configFilesystem;
private Filesystem $certificateStoreLocal;
private ?Filesystem $certificateStoreRemote;
private Logger $logger;
private string $instanceStateHash = '';
private array $fileHashes;
public function __construct()
{
$this->environment = array_merge($_ENV, $_SERVER);
ksort($this->environment);
$this->logger = new Monolog\Logger('bouncer');
$this->logger->pushHandler(new StreamHandler('/var/log/bouncer.log', Logger::DEBUG));
$stdout = new StreamHandler('php://stdout', Logger::DEBUG);
$stdout->setFormatter(new ColoredLineFormatter(null, "%level_name%: %message% \n"));
$this->logger->pushHandler($stdout);
$this->client = new Guzzle(
[
'base_uri' => 'http://localhost',
'curl' => [
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock',
],
]
);
$this->loader = new FilesystemLoader([
__DIR__,
]);
$this->twig = new Environment($this->loader);
// Set up Filesystem for sites-enabled path
$this->configFilesystem = new Filesystem(new LocalFilesystemAdapter('/etc/nginx/sites-enabled'));
// Set up Local certificate store
$this->certificateStoreLocal = new Filesystem(new LocalFilesystemAdapter('/etc/letsencrypt'));
// Set up Remote certificate store, if configured
if ($this->environment['BOUNCER_S3_BUCKET']) {
$this->certificateStoreRemote = new Filesystem(
new AwsS3V3Adapter(
new S3Client([
'endpoint' => $this->environment['BOUNCER_S3_ENDPOINT'],
'use_path_style_endpoint' => isset($this->environment['BOUNCER_S3_USE_PATH_STYLE_ENDPOINT']),
'credentials' => [
'key' => $this->environment['BOUNCER_S3_KEY_ID'],
'secret' => $this->environment['BOUNCER_S3_KEY_SECRET'],
],
'region' => $this->environment['BOUNCER_S3_REGION'] ?? 'us-east',
'version' => 'latest',
]),
$this->environment['BOUNCER_S3_BUCKET'],
$this->environment['BOUNCER_S3_PREFIX'] ?? ''
)
);
}
}
/**
* @throws \GuzzleHttp\Exception\GuzzleException
*
* @return BouncerTarget[]
*/
public function findContainers(): array
{
$bouncerTargets = [];
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
foreach ($containers as $container) {
$envs = [];
$inspect = json_decode($this->client->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
if (isset($inspect['Config']['Env'])) {
foreach ($inspect['Config']['Env'] as $environmentItem) {
if (stripos($environmentItem, '=') !== false) {
[$envKey, $envVal] = explode('=', $environmentItem, 2);
$envs[$envKey] = $envVal;
} else {
$envs[$envKey] = true;
}
}
}
if (isset($envs['BOUNCER_DOMAIN'])) {
$bouncerTarget = (new BouncerTarget())
->setId($inspect['Id'])
;
foreach ($envs as $eKey => $eVal) {
switch ($eKey) {
case 'BOUNCER_DOMAIN':
$domains = explode(',', $eVal);
array_walk($domains, function (&$domain, $key): void { $domain = trim($domain); });
$bouncerTarget->setDomains($domains);
break;
case 'BOUNCER_LETSENCRYPT':
$bouncerTarget->setLetsEncrypt(in_array(strtolower($eVal), ['yes', 'true'], true));
break;
case 'BOUNCER_TARGET_PORT':
$bouncerTarget->setPort($eVal);
break;
case 'BOUNCER_ALLOW_NON_SSL':
$bouncerTarget->setAllowNonSSL(in_array(strtolower($eVal), ['yes', 'true'], true));
break;
}
}
if (isset($inspect['NetworkSettings']['IPAddress']) && !empty($inspect['NetworkSettings']['IPAddress'])) {
// As per docker service
$bouncerTarget->setIp($inspect['NetworkSettings']['IPAddress']);
} else {
// As per docker compose
$networks = array_values($inspect['NetworkSettings']['Networks']);
$bouncerTarget->setIp($networks[0]['IPAddress']);
}
$this->logger->debug(sprintf('Decided that %s has the ip %s', $bouncerTarget->getName(), $bouncerTarget->getIp()));
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getIp(), $bouncerTarget->getPort()));
$bouncerTargets[] = $bouncerTarget;
}
}
return $bouncerTargets;
}
public function run(): void
{
$this->logger->info(sprintf('%s Starting Bouncer...', Emoji::CHARACTER_TIMER_CLOCK));
$this->stateHasChanged();
while (true) {
$this->runLoop();
}
}
/**
* Returns true when something has changed.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
private function stateHasChanged(): bool
{
$newInstanceStates = [];
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
foreach ($containers as $container) {
$inspect = json_decode($this->client->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
$newInstanceStates[$inspect['Id']] = implode('::', [
$inspect['Name'],
$inspect['Created'],
$inspect['Image'],
$inspect['State']['Status'],
sha1(implode('|', $inspect['Config']['Env'])),
]);
}
$newStateHash = sha1(implode("\n", $newInstanceStates));
//$this->logger->debug(sprintf("Old state = %s. New State = %s.", substr($this->instanceStateHash,0,7), substr($newStateHash, 0,7)));
if ($this->instanceStateHash != $newStateHash) {
$this->instanceStateHash = $newStateHash;
return true;
}
return false;
}
private function runLoop(): void
{
if ($this->s3Enabled()) {
$this->getCertificatesFromS3();
}
$targets = $this->findContainers();
$this->logger->info(sprintf('%s Found %d services with BOUNCER_DOMAIN set', Emoji::CHARACTER_MAGNIFYING_GLASS_TILTED_LEFT, count($targets)));
foreach ($targets as $target) {
$this->generateNginxConfig($target);
}
$this->generateLetsEncryptCerts($targets);
if ($this->s3Enabled()) {
$this->writeCertificatesToS3();
}
$this->waitUntilContainerChange();
}
private function waitUntilContainerChange(): void
{
while ($this->stateHasChanged() === false) {
sleep(5);
}
$this->logger->debug(sprintf('%s Host Container state has changed', Emoji::CHARACTER_WARNING));
}
private function s3Enabled(): bool
{
return $this->certificateStoreRemote instanceof Filesystem;
}
private function getCertificatesFromS3(): void
{
$this->logger->info(sprintf('%s Downloading Certificates from S3', Emoji::CHARACTER_DOWN_ARROW));
foreach ($this->certificateStoreRemote->listContents('/', true) as $file) {
/** @var FileAttributes $file */
if ($file->isFile()) {
$localPath = "archive/{$file->path()}";
#$this->logger->debug(sprintf(" > Downloading {$file->path()} "));
$this->certificateStoreLocal->writeStream($localPath, $this->certificateStoreRemote->readStream($file->path()));
$this->fileHashes[$localPath] = sha1($this->certificateStoreLocal->read($localPath));
}
}
// Copy certs into /live because certbot is a pain.
foreach ($this->certificateStoreLocal->listContents('/archive', true) as $newLocalCert) {
/** @var FileAttributes $newLocalCert */
if ($newLocalCert->isFile() && pathinfo($newLocalCert->path(), PATHINFO_EXTENSION) == 'pem') {
$livePath = str_replace('archive/', 'live/', $newLocalCert->path());
$this->certificateStoreLocal->writeStream($livePath, $this->certificateStoreLocal->readStream($newLocalCert->path()));
}
}
}
private function fileChanged(string $localPath)
{
if (!isset($this->fileHashes[$localPath])) {
return true;
}
if (sha1($this->certificateStoreLocal->read($localPath)) != $this->fileHashes[$localPath]) {
return true;
}
return false;
}
private function writeCertificatesToS3(): void
{
$this->logger->info(sprintf('%s Uploading Certificates to S3', Emoji::CHARACTER_UP_ARROW));
foreach ($this->certificateStoreLocal->listContents('/archive', true) as $file) {
/** @var FileAttributes $file */
if ($file->isFile()) {
$remotePath = str_replace('archive/', '', $file->path());
if (!$this->certificateStoreRemote->fileExists($remotePath) || $this->fileChanged($file->path())) {
#$this->logger->debug(sprintf(" > Uploading {$file->path()} "));
$this->certificateStoreRemote->writeStream($remotePath, $this->certificateStoreLocal->readStream($file->path()));
} else {
#$this->logger->debug(sprintf(" > Skipping uploading {$file->path()}, file not changed."));
}
}
}
}
private function generateNginxConfig(BouncerTarget $target): self
{
$this->configFilesystem->write(
$target->getName(),
$this->twig->render('NginxTemplate.twig', $target->__toArray())
);
$this->logger->info(sprintf('%s Created config for %s', Emoji::CHARACTER_PENCIL, $target->getName()));
return $this;
}
/**
* @param BouncerTarget[] $targets
*
* @return $this
*/
private function generateLetsEncryptCerts(array $targets): self
{
foreach ($targets as $target) {
if (!$target->isLetsEncrypt()) {
continue;
}
$testAgeFile = "/archive/{$target->getName()}/fullchain1.pem";
if ($this->certificateStoreLocal->fileExists($testAgeFile)) {
$ssl = openssl_x509_parse($this->certificateStoreLocal->read($testAgeFile));
$timeRemainingSeconds = $ssl['validTo_time_t'] - time();
if ($timeRemainingSeconds > 2592000) {
$this->logger->info(sprintf(
'%s Skipping %s, certificate is still good for %d days',
Emoji::CHARACTER_PARTYING_FACE,
$target->getName(),
round($timeRemainingSeconds / 86400)
));
continue;
}
}
$shell = new Exec();
$command = new CommandBuilder('/usr/bin/certbot');
$command->addSubCommand('certonly');
$command->addArgument('nginx');
if ($this->environment['BOUNCER_LETSENCRYPT_MODE'] != 'production') {
$command->addArgument('test-cert');
}
$command->addFlag('d', implode(',', $target->getDomains()));
$command->addFlag('n');
$command->addFlag('m', $this->environment['BOUNCER_LETSENCRYPT_EMAIL']);
$command->addArgument('agree-tos');
$this->logger->info(sprintf('%s Generating letsencrypt for %s - %s', Emoji::CHARACTER_PENCIL, $target->getName(), $command->__toString()));
$shell->run($command);
if ($shell->getReturnValue() == 0) {
$this->logger->info(sprintf('%s Generating successful', Emoji::CHARACTER_PARTY_POPPER));
} else {
$this->logger->critical(sprintf('%s Generating failed!', Emoji::CHARACTER_WARNING));
}
$target->setUseTemporaryCert(false);
$this->generateNginxConfig($target);
}
$this->restartNginx();
return $this;
}
private function restartNginx(): void
{
$shell = new Exec();
$command = new CommandBuilder('/usr/sbin/nginx');
$command->addFlag('s', 'reload');
$this->logger->info(sprintf('%s Restarting nginx', Emoji::CHARACTER_TIMER_CLOCK));
$shell->run($command);
}
}
(new Bouncer())->run();

3
bouncer/bouncer.runit Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "Starting Bouncer"
/app/bouncer

32
bouncer/composer.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "benzine/bouncer",
"description": "Nginx Configuration Management",
"type": "project",
"config": {
"sort-packages": true
},
"license": "GPL-3.0-or-later",
"require": {
"php": "^8.0",
"ext-json": "*",
"ext-curl": "*",
"kint-php/kint": "^3.3",
"guzzlehttp/guzzle": "^7.3",
"twig/twig": "^3.0",
"league/flysystem": "^2.1",
"monolog/monolog": "^2.2",
"bramus/monolog-colored-line-formatter": "~3.0",
"adambrett/shell-wrapper": "dev-master",
"league/flysystem-aws-s3-v3": "^2.1",
"spatie/emoji": "^2.3"
},
"authors": [
{
"name": "Matthew Baggett",
"email": "matthew@baggett.me"
}
],
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0"
}
}

3209
bouncer/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
version: "3.4"
services:
bouncer:
image: benzine/bouncer
build: .
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./:/app
environment:
- BOUNCER_LETSENCRYPT_MODE=staging
- BOUNCER_LETSENCRYPT_EMAIL=matthew@baggett.me
- BOUNCER_S3_ENDPOINT=http://grey.ooo:9000
- BOUNCER_S3_KEY_ID=geusebio
- BOUNCER_S3_KEY_SECRET=changeme
- BOUNCER_S3_BUCKET=bouncer-certificates
- BOUNCER_S3_USE_PATH_STYLE_ENDPOINT="yes"
ports:
- 127.0.99.100:80:80
- 127.0.99.100:443:443
web-a:
image: benzine/php:nginx
volumes:
- ./test/public-web-a:/app/public
environment:
- BOUNCER_DOMAIN=a.web.grey.ooo
- BOUNCER_LETSENCRYPT=true
# web-b:
# image: benzine/php:nginx
# volumes:
# - ./test/public-web-b:/app/public
# environment:
# - BOUNCER_DOMAIN=b.web.grey.ooo
#
#

43
bouncer/grey-ooo-test.yml Normal file
View file

@ -0,0 +1,43 @@
version: "3.4"
services:
bouncer:
image: benzine/bouncer
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- BOUNCER_LETSENCRYPT_MODE=production
- BOUNCER_LETSENCRYPT_EMAIL=matthew@baggett.me
- BOUNCER_S3_ENDPOINT=http://grey.ooo:9000
- BOUNCER_S3_KEY_ID=geusebio
- BOUNCER_S3_KEY_SECRET=teblE0neTf2NQcVFaZIRkSF44RscyQ3G
- BOUNCER_S3_BUCKET=bouncer-certificates
- BOUNCER_S3_USE_PATH_STYLE_ENDPOINT="yes"
ports:
- 80:80
- 443:443
web-a:
image: benzine/php:nginx
volumes:
- ./test/public-web-a:/app/public
environment:
- BOUNCER_DOMAIN=a.web.grey.ooo
- BOUNCER_LETSENCRYPT=true
web-b:
image: benzine/php:nginx
volumes:
- ./test/public-web-b:/app/public
environment:
- BOUNCER_DOMAIN=b.web.grey.ooo
- BOUNCER_LETSENCRYPT=true
web-c:
image: benzine/php:nginx
volumes:
- ./test/public-web-c:/app/public
environment:
- BOUNCER_DOMAIN=c.web.grey.ooo
- BOUNCER_LETSENCRYPT=true

View file

@ -0,0 +1,2 @@
#!/usr/bin/env bash
tail -f /var/log/nginx/access.log | sed --unbuffered 's|.*\[.*\] |[NGINX] |g' | grep -v /v1/ping

2
bouncer/logs-nginx-error.runit Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env bash
tail -f /var/log/nginx/error.log

6
bouncer/logs.runit Normal file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
if [[ -f /var/log/bouncer/*.log ]]; then
tail -f /var/log/bouncer/*.log
else
sleep 1
fi

3
bouncer/nginx.runit Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "Starting Nginx"
/usr/sbin/nginx

View file

@ -0,0 +1 @@
Nothing here.

View file

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDnTCCAoWgAwIBAgIUJYUUmBQ2/ERZ7xinAJzVhiFWViYwDQYJKoZIhvcNAQEL
BQAwXTELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0Zsb3JpZGExDjAMBgNVBAcMBU1p
YW1pMRYwFAYDVQQKDA1FeGFtcGxlIEdyb3VwMRQwEgYDVQQDDAtleGFtcGxlLm9y
ZzAgFw0yMTA1MzAxNzU4MzlaGA8yMTIxMDUwNjE3NTgzOVowXTELMAkGA1UEBhMC
VVMxEDAOBgNVBAgMB0Zsb3JpZGExDjAMBgNVBAcMBU1pYW1pMRYwFAYDVQQKDA1F
eGFtcGxlIEdyb3VwMRQwEgYDVQQDDAtleGFtcGxlLm9yZzCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBANJa9OcoCW+mej8qDMCCTGnqMAuUqBIj1wZLgOdT
4DHriq1vKi1JLsDZkYekrCq/sfWo97kDXsdK6YN4+mua5EN4cTG3mSpal+RgLTc2
HMKHFfgzPzIN/n5AEqzdVZb5j0P3LoUNH687AlplW0BB+K64Gw//2KPx0Q8Fkhq2
I97V8SRpqds78PJHzhfuZNs/AUFpFXnYHJyO2Q63Btq2aoTMQyoLDRBBxin70II2
6Cjh3k6EhMY+HuYS1AjfI8cDQw289asJBLa6zPoD0VGaGNfCSrOzxrUqfhIoOkuY
W7rOIsK6rSSu1neSKQIiOLVjQxifxrQIIKTQhRiSplgD9LUCAwEAAaNTMFEwHQYD
VR0OBBYEFADK74w4AGeETK72k/htsnol9ye0MB8GA1UdIwQYMBaAFADK74w4AGeE
TK72k/htsnol9ye0MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AKElv0xx95lD2leXEOfD6DKakrzuE8lONmcrkfjehTOd7jbqblnj8u1DCWytwB8P
gEr5FXve0iy7avGoNkU33MufbbQokAMoTs/IA+rwMfv0unupT1aYN8TTEXJJ100j
MXBsq/PvNkBNwkBcXjYHHsVjdM3bptbaw9A4V9opfMjQXAY5wuk3rBBm8On2rJKy
Qksh/uLoe8wbZ5dvLv9oc9sRpIilaSy8TcbrHkDIaWA5WCdVFfcayDGYdjhCYLGW
tj/48g0THvJv6JvVYwFJqTM690YUSlxaOHQE2ZneLytocVyAdEL2MMldRezvtI1z
1OXOia2G7koNYtS7cD8G1IM=
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDSWvTnKAlvpno/
KgzAgkxp6jALlKgSI9cGS4DnU+Ax64qtbyotSS7A2ZGHpKwqv7H1qPe5A17HSumD
ePprmuRDeHExt5kqWpfkYC03NhzChxX4Mz8yDf5+QBKs3VWW+Y9D9y6FDR+vOwJa
ZVtAQfiuuBsP/9ij8dEPBZIatiPe1fEkaanbO/DyR84X7mTbPwFBaRV52BycjtkO
twbatmqEzEMqCw0QQcYp+9CCNugo4d5OhITGPh7mEtQI3yPHA0MNvPWrCQS2usz6
A9FRmhjXwkqzs8a1Kn4SKDpLmFu6ziLCuq0krtZ3kikCIji1Y0MYn8a0CCCk0IUY
kqZYA/S1AgMBAAECggEARqfQjPgwuzTi6OZ55AugGQ9VVf53uagaKH4h7RGKQ5pH
OVwWgaGMN7CcpkAUqEM9RjOcCaPtKOmrp8Jx8sTTGSqScs2lf8lwLYB0j4/4dwqi
wXyNJIX4znU9EJ1Di3OFwKF9Gam/077xWmWjEeFW43DpfiVEokSuIOqRGbHGOKlt
2ygHJu+rmPapEPyYqSWQnAkYX0DW/KCAGiyIAqph/SgrCDTdsxbNOa2OwDygPC54
7xW0yCduvgFLh9bxedF8iifzRkPw710cxyqVsYwHiwugDgxL4NiK1DlWbpBimab5
ocye9+ElymMZ8DTjpA85cXny/TtoqJfqTs1YGYgrvQKBgQDwHnAcY0BjQ4o+ZneG
oqBJeQ8KCMRU4pEIa5QOOeUr46gtiPIfcFh/BJUHQ61qk7gcJj5BV2GXNS7+m+sU
RC3Usblm9twwxZn7mfoOk4z9NEfBI2MXmbB8ARjAQBCost+3KQAoSIL1AyDKiAlY
2JfMt+73+kwUsg7b9g0pYIfn/wKBgQDgRJPlSIxJs2mbjzUwVBAeslct2W0dehrh
V0sXPxEhJHWX6P343vLqRHRsKgqhbU/vy+3JrIS9ftwGKcmb+Y9EJgYrR+D3ZYzs
idSOsunSspJgbCG5mHE1VQhr8IpHeCkuSt22aFErLfsjzXWZIewK2tqZN1QUjdc5
EJHOD4UDSwKBgFYRYvgZ72NlOzFAw0kkE7YiSWy8Vbtjdr8A6JHs2KNRt9+Sfc8d
Eut8dfqjnI5eIpkccCY1rwpnCtBCjRG3moHprl4k0Co/OgGAYKxG4TuFOM8W4xb7
hNH+BqQqko4Vh7D8Zk0KKL6v/1n5RvhssoSzzVlfg1PLux3G5VLWggB7AoGAAP/N
OORN27Y07kCBGCoHuFtLECU72znEDOT6rKvXQ7KJ45diKk2z/182tZSqX3XBOWxL
Lu7Z2I5MJKri/xLplIAm3uJ/GhsVuagTjl81s36gMFXLAKyxNG+gjfqQYykh5dbn
jfyBABRAXjR4JaqFBrda6fvZIA5RuytbuvNOwGkCgYAUs82tDGLiqyMPd2jgYS3k
aL62f0TLKHjmTCmRca7IqXbqcMbAj+LgAHI2HfCfjc4KWd68ZGRLcpDlehMcis1f
PQi3HW+2b9dAZX6+HAIGiVem//ckYXgUza4MMosh0hXquGs1yJ/VNWC+HPIHrj6X
9tvvvHnGKav329q/Z/8K/A==
-----END PRIVATE KEY-----

View file

@ -0,0 +1 @@
<h1>Website A</h1>

View file

@ -0,0 +1 @@
<h1>Website B</h1>

View file

@ -0,0 +1 @@
<h1>Website C</h1>

View file

@ -11,7 +11,6 @@ ENV DEBIAN_FRONTEND="teletype" \
COLOUR_NONE='\e[39m' \
DEFAULT_TZ='Europe/London'
CMD ["runsvdir", "-P", "/etc/service"]
WORKDIR /app
ENV PATH="/app:/app/bin:/app/vendor/bin:${PATH}"
@ -20,5 +19,8 @@ COPY installers /installers
COPY etc /etc
COPY usr /usr
CMD ["/usr/bin/marshall"]
RUN /installers/install && \
rm -rf /marshall /installers
rm -rf /marshall /installers && \
chmod +x /usr/bin/marshall

7
marshall/usr/bin/marshall Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Fix for windows hosts manging run files
dos2unix -q /etc/service/*/run
# Start Runit.
runsvdir -P /etc/service