Merge pull request #127 from copenhagentruckwash/scalability-enhancement
scalability-enhancement
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
## Environment overrides for docker-compose
|
||||
|
||||
# Path to the host's Let's Encrypt directory that contains live/<domain>/ certs.
|
||||
# This directory is bind-mounted into the Nginx container at /etc/letsencrypt.
|
||||
|
||||
# Linux hosts (default)
|
||||
LETSENCRYPT_PATH=/etc/letsencrypt
|
||||
|
||||
# Windows (Docker Desktop with WSL Ubuntu). Uncomment and adjust distro name if different.
|
||||
# LETSENCRYPT_PATH=//wsl$/Ubuntu/etc/letsencrypt
|
||||
|
||||
# Notes:
|
||||
# - Ensure the following files exist under the path you set:
|
||||
# live/api.truckwash.dk/fullchain.pem
|
||||
# live/api.truckwash.dk/privkey.pem
|
||||
# live/cloud.truckwash.dk/fullchain.pem
|
||||
# live/cloud.truckwash.dk/privkey.pem
|
||||
# - If you do not have certs for cloud.truckwash.dk locally, either comment out that
|
||||
# TLS server block in services/nginx/nginx.conf or place a temporary self-signed
|
||||
# cert/key pair at the expected path.
|
||||
+34
-4
@@ -9,6 +9,10 @@ RUN apt-get update && apt-get install -y \
|
||||
libpq-dev \
|
||||
libzip-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libpng-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libfreetype6-dev \
|
||||
libxml2-dev \
|
||||
default-mysql-client \
|
||||
redis-server \
|
||||
nginx \
|
||||
@@ -20,6 +24,14 @@ RUN apt-get update && apt-get install -y \
|
||||
bcmath \
|
||||
sockets
|
||||
|
||||
# Build and enable common extensions required by Composer dependencies
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) gd mbstring dom simplexml
|
||||
|
||||
# Install and enable PHP Redis extension for session storage
|
||||
RUN pecl install redis \
|
||||
&& docker-php-ext-enable redis
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
@@ -32,8 +44,23 @@ COPY nginx.conf /etc/nginx/nginx.conf
|
||||
# Install Composer
|
||||
COPY --from=composer:2.6 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Install PHP dependencies through Composer
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
# Runtime bootstrap: lightweight entrypoint to ensure Composer deps exist when app is bind-mounted
|
||||
COPY services/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Install PHP dependencies through Composer (only where composer.json exists)
|
||||
# Main app dependencies
|
||||
RUN set -e \
|
||||
&& if [ -f /var/www/html/services/nginx/app/composer.json ]; then \
|
||||
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader -d /var/www/html/services/nginx/app; \
|
||||
else \
|
||||
echo "No composer.json in /var/www/html/services/nginx/app — skipping"; \
|
||||
fi \
|
||||
&& if [ -f /var/www/html/services/nginx/app/modules/washcertificates/composer.json ]; then \
|
||||
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader -d /var/www/html/services/nginx/app/modules/washcertificates; \
|
||||
else \
|
||||
echo "No composer.json in washcertificates module — skipping"; \
|
||||
fi
|
||||
|
||||
# Set permissions for the web server
|
||||
RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html
|
||||
@@ -41,5 +68,8 @@ RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html
|
||||
# Expose port 80 (HTTP) and 443 (HTTPS)
|
||||
EXPOSE 80 443
|
||||
|
||||
# Start services (Nginx and Redis) when the container starts
|
||||
CMD service redis-server start && nginx -g "daemon off;" && php-fpm
|
||||
# Default entrypoint ensures vendor/autoload.php exists, then executes the given command (overridden by docker-compose)
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
|
||||
# Start services when no command is provided (docker-compose overrides this with ["php-fpm"])
|
||||
CMD ["php-fpm"]
|
||||
@@ -0,0 +1,35 @@
|
||||
### Local TLS certificates with Docker Compose
|
||||
|
||||
Nginx is configured to terminate TLS using certificates mounted at `/etc/letsencrypt` inside the container. To use your real Let’s Encrypt certificates on the host, the Compose file mounts a configurable path via `LETSENCRYPT_PATH`.
|
||||
|
||||
#### 1) Configure the host certificate path
|
||||
Copy `.env.example` to `.env` in the project root and adjust the path for your environment:
|
||||
|
||||
```
|
||||
cp .env.example .env
|
||||
# Edit .env and set one of the following:
|
||||
# Linux: LETSENCRYPT_PATH=/etc/letsencrypt
|
||||
# Windows (WSL Ubuntu): LETSENCRYPT_PATH=//wsl$/Ubuntu/etc/letsencrypt
|
||||
```
|
||||
|
||||
Ensure the following files exist under the path you set:
|
||||
- `live/api.truckwash.dk/fullchain.pem`
|
||||
- `live/api.truckwash.dk/privkey.pem`
|
||||
- `live/cloud.truckwash.dk/fullchain.pem`
|
||||
- `live/cloud.truckwash.dk/privkey.pem`
|
||||
|
||||
If you don’t have certs for `cloud.truckwash.dk` locally, either comment out that TLS server block in `services/nginx/nginx.conf` or place a temporary self‑signed cert/key at the expected path.
|
||||
|
||||
#### 2) Start Nginx
|
||||
```
|
||||
docker compose up -d nginx
|
||||
```
|
||||
|
||||
Check logs:
|
||||
```
|
||||
docker compose logs -f nginx
|
||||
```
|
||||
|
||||
#### 3) Troubleshooting
|
||||
- “cannot load certificate … no such file or directory”: verify `LETSENCRYPT_PATH` and that all `live/<domain>/` files exist on the host.
|
||||
- For purely local testing without HTTPS, you may comment out the HTTPS server blocks and the HTTP→HTTPS redirect in `services/nginx/nginx.conf` and run on port 80.
|
||||
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
traefik:
|
||||
# Use hardened Traefik config in production (no staging CA, no tracing/metrics)
|
||||
volumes:
|
||||
- ./services/traefik/traefik.prod.yml:/etc/traefik/traefik.yml:ro
|
||||
- ./services/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||
- ./services/traefik/acme.json:/acme.json
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
redis:
|
||||
ports: []
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
## Usage (examples):
|
||||
## - With explicit files (recommended):
|
||||
## docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d traefik caddy php1 php2 php3 php4 php5 db redis
|
||||
##
|
||||
## - Or set COMPOSE_FILE for the shell session (PowerShell on Windows):
|
||||
## $env:COMPOSE_FILE = "docker-compose.yml;docker-compose.prod.yml"
|
||||
## docker compose up -d traefik caddy php1 php2 php3 php4 php5 db redis
|
||||
##
|
||||
## Notes:
|
||||
## - Traefik uses Let’s Encrypt production. Ensure DNS A/AAAA records for api.truckwash.dk and traefik.truckwash.dk point to this host and ports 80/443 are reachable.
|
||||
## - The dashboard is protected by basic auth and an IP allowlist (defined in dynamic.yml). Replace the bcrypt hash before enabling in production.
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
# Traefik terminates TLS; Caddy should serve plain HTTP internally
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:80 {
|
||||
encode gzip
|
||||
root * /var/www/html
|
||||
|
||||
# CORS is handled at the edge by Traefik's headers middleware.
|
||||
# Do not set or strip Access-Control-* headers here to avoid conflicts.
|
||||
|
||||
# PHP handling via FastCGI to php-fpm pool
|
||||
php_fastcgi php1:9000 php2:9000 php3:9000 php4:9000 php5:9000
|
||||
|
||||
try_files {path} {path}/ /index.php
|
||||
file_server
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/access.log {
|
||||
roll_size 10MiB
|
||||
roll_keep 5
|
||||
roll_keep_for 720h
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
strict.perms: false
|
||||
|
||||
filebeat.inputs:
|
||||
- type: filestream
|
||||
id: nginx-access
|
||||
paths: ["/var/log/nginx/*.ndjson"]
|
||||
parsers:
|
||||
- ndjson:
|
||||
overwrite_keys: true
|
||||
add_error_key: true
|
||||
fields:
|
||||
service: nginx
|
||||
log_type: access
|
||||
fields_under_root: true
|
||||
|
||||
- type: filestream
|
||||
id: nginx-error
|
||||
paths: ["/var/log/nginx/error.log"]
|
||||
fields:
|
||||
service: nginx
|
||||
log_type: error
|
||||
fields_under_root: true
|
||||
|
||||
- type: filestream
|
||||
id: php-error
|
||||
paths: ["/var/log/php/*.log"]
|
||||
fields:
|
||||
service: php
|
||||
log_type: error
|
||||
fields_under_root: true
|
||||
|
||||
setup.kibana.host: "http://kibana:5601"
|
||||
setup.kibana.username: elastic
|
||||
setup.kibana.password: ${ELASTIC_PASSWORD}
|
||||
|
||||
output.elasticsearch:
|
||||
hosts: ["http://elasticsearch:9200"]
|
||||
username: elastic
|
||||
password: ${ELASTIC_PASSWORD}
|
||||
@@ -0,0 +1,48 @@
|
||||
metricbeat.config.modules:
|
||||
path: ${path.config}/modules.d/*.yml
|
||||
reload.enabled: false
|
||||
|
||||
setup.kibana.host: "http://kibana:5601"
|
||||
setup.kibana.username: elastic
|
||||
setup.kibana.password: ${ELASTIC_PASSWORD}
|
||||
setup.dashboards.enabled: true
|
||||
|
||||
output.elasticsearch:
|
||||
hosts: ["http://elasticsearch:9200"]
|
||||
username: elastic
|
||||
password: ${ELASTIC_PASSWORD}
|
||||
|
||||
metricbeat.modules:
|
||||
# System metrics (container-level when running in Docker Desktop)
|
||||
- module: system
|
||||
period: 10s
|
||||
metricsets:
|
||||
- cpu
|
||||
- load
|
||||
- memory
|
||||
- network
|
||||
- process
|
||||
- process_summary
|
||||
- filesystem
|
||||
- fsstat
|
||||
- uptime
|
||||
processes: ['.*']
|
||||
|
||||
# Docker metrics (containers CPU/mem/network, etc.)
|
||||
- module: docker
|
||||
hosts: ["unix:///var/run/docker.sock"]
|
||||
period: 10s
|
||||
enabled: true
|
||||
# Common metricsets: container,cpu,diskio,event,healthcheck,info,memory,network
|
||||
# Use defaults to keep noise reasonable
|
||||
|
||||
# Nginx metrics from stub_status (enabled in nginx.dev.conf on port 8080)
|
||||
- module: nginx
|
||||
metricsets: ["stubstatus"]
|
||||
period: 10s
|
||||
hosts: ["http://nginx:8080/stub_status"]
|
||||
enabled: true
|
||||
|
||||
processors:
|
||||
- add_host_metadata: ~
|
||||
- add_docker_metadata: ~
|
||||
@@ -2,7 +2,7 @@
|
||||
FROM nginx:1.27.3-alpine
|
||||
|
||||
# Import the configuration file for Nginx
|
||||
COPY /nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy the contents of the /app directory to the /var/www/html directory (This is not necessary for this, since we are using PHP and Nginx in separate containers - And we are not using Nginx to serve static files)
|
||||
#COPY /app /var/www/html
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"php-http/guzzle7-adapter": "^1.1",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"mailersend/mailersend": "^0.28.0",
|
||||
"spipu/html2pdf": "^5.3",
|
||||
"microsoft/microsoft-graph": "2.7.0"
|
||||
"spipu/html2pdf": "^5.3"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
||||
Generated
+263
-1646
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Customer-Nu
|
||||
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
|
||||
|
||||
/** Autoload */
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
/** Load all Interfaces */
|
||||
foreach ( glob(WD . '/interfaces/*.php') as $interface ) {
|
||||
require_once $interface;
|
||||
|
||||
@@ -26,8 +26,8 @@ class exampleRoute
|
||||
// This is very hack-y, but it works for now
|
||||
// Check if it's monday
|
||||
if ((int)date('N') === 1) {
|
||||
// Check if the time is between 06:00 and 17:00
|
||||
if (!redis->exists('system:last_sent_monday_message') && (date('H') >= 6 && date('H') < 17)) {
|
||||
// Check if the time is between 07:00 and 17:00
|
||||
if (!redis->exists('system:last_sent_monday_message') && (date('H') >= 7 && date('H') < 17)) {
|
||||
// Set the key to expire in 24 hours
|
||||
redis->set('system:last_sent_monday_message', date('Y-m-d H:i:s'));
|
||||
redis->expire('system:last_sent_monday_message', 86400);
|
||||
@@ -55,6 +55,38 @@ class exampleRoute
|
||||
$response->success(['message' => 'Hello World!']);
|
||||
});
|
||||
|
||||
$this->get('/tmp-washes-in-time', function () {
|
||||
global $response;
|
||||
$response->error(['message' => 'This route is deprecated.']);
|
||||
$date_from = date('2026-01-01 00:00:00');
|
||||
$date_to = date('2026-01-31 23:59:59');
|
||||
// Set the hours on the date range to be from 17:00-23:59
|
||||
$daily_start_time = '17:00:00';
|
||||
$daily_end_time = '23:59:59';
|
||||
// Loop through each day in the date range, and get the washes that were done in the time range
|
||||
$washes = [];
|
||||
$current_date = $date_from;
|
||||
while (strtotime($current_date) <= strtotime($date_to)) {
|
||||
$daily_start = date('Y-m-d', strtotime($current_date)) . ' ' . $daily_start_time;
|
||||
$daily_end = date('Y-m-d', strtotime($current_date)) . ' ' . $daily_end_time;
|
||||
$daily_washes = (new orders_o())->getWashesInTimeRange($daily_start, $daily_end, ['department_id' => 7]);
|
||||
$washes = array_merge($washes, $daily_washes);
|
||||
$current_date = date('Y-m-d H:i:s', strtotime($current_date . ' +1 day'));
|
||||
}
|
||||
$wash_arrays = array_map(function ($wash) {
|
||||
return $wash->asArray();
|
||||
}, $washes);
|
||||
// Create a CSV file from the washes $csv = "Order ID,Customer ID,Department ID,Created At\n";
|
||||
foreach ($wash_arrays as $wash) {
|
||||
$csv .= "{$wash['id']},{$wash['customer_id']},{$wash['department_id']},{$wash['created_at']}\n";
|
||||
}
|
||||
// Output the CSV file
|
||||
header('Content-Type: text/csv');
|
||||
header('Content-Disposition: attachment; filename="washes_in_time_range.csv"');
|
||||
echo $csv;
|
||||
exit;
|
||||
});
|
||||
|
||||
$this->get('/tmp-customer-list-overcharged', function () {
|
||||
global $response;
|
||||
$response->error(['message' => 'This route is deprecated.']);
|
||||
|
||||
@@ -14,9 +14,13 @@ http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Structured JSON access logs and correlation id
|
||||
map $http_x_request_id $req_id { default $http_x_request_id; "" $request_id; }
|
||||
log_format json escape=json '{"time":"$time_iso8601","req_id":"$req_id","remote":"$remote_addr","host":"$host","method":"$request_method","uri":"$request_uri","status":$status,"bytes":$bytes_sent,"referer":"$http_referer","ua":"$http_user_agent","up_addr":"$upstream_addr","up_st":"$upstream_status","up_rt":$upstream_response_time,"rt":$request_time}';
|
||||
|
||||
# Log settings
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.ndjson json;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# Gzip settings (optional)
|
||||
gzip on;
|
||||
@@ -25,6 +29,17 @@ http {
|
||||
# Include server block configurations
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
# Upstream pool for PHP-FPM (load-balanced)
|
||||
upstream php_fpm {
|
||||
least_conn;
|
||||
server php1:9000 max_fails=3 fail_timeout=10s;
|
||||
server php2:9000 max_fails=3 fail_timeout=10s;
|
||||
server php3:9000 max_fails=3 fail_timeout=10s;
|
||||
server php4:9000 max_fails=3 fail_timeout=10s;
|
||||
server php5:9000 max_fails=3 fail_timeout=10s;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
@@ -34,6 +49,55 @@ http {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# Localhost api server block (api.truckwash.dk)
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Logging
|
||||
add_header X-Request-Id $req_id always;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.ndjson json;
|
||||
|
||||
# Index file
|
||||
index index.php index.html index.htm;
|
||||
|
||||
# Root directory (public- uses index.php)
|
||||
root /var/www/html;
|
||||
|
||||
# Location block for PHP files
|
||||
location ^~ / {
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, X-Customer-Number";
|
||||
add_header Access-Control-Allow-Credentials true;
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, X-Customer-Number";
|
||||
return 204;
|
||||
}
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php_fpm;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_read_timeout 60s;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
fastcgi_param X_REQUEST_ID $req_id;
|
||||
fastcgi_keep_conn on;
|
||||
}
|
||||
|
||||
# Deny access to .htaccess files
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Deny access to hidden files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name api.truckwash.dk;
|
||||
@@ -48,8 +112,9 @@ http {
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# Logging
|
||||
add_header X-Request-Id $req_id always;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.log combined;
|
||||
access_log /var/log/nginx/access.ndjson json;
|
||||
|
||||
# Index file
|
||||
index index.php index.html index.htm;
|
||||
@@ -58,7 +123,6 @@ http {
|
||||
root /var/www/html;
|
||||
|
||||
# Location block for PHP files
|
||||
|
||||
location ^~ / {
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, X-Customer-Number";
|
||||
@@ -72,10 +136,12 @@ http {
|
||||
}
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_pass php_fpm;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_read_timeout 60;
|
||||
fastcgi_param SCRIPT_FILENAME index.php;
|
||||
fastcgi_read_timeout 60s;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
fastcgi_param X_REQUEST_ID $req_id;
|
||||
fastcgi_keep_conn on;
|
||||
}
|
||||
|
||||
# Deny access to .htaccess files
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Structured JSON access logs and correlation id
|
||||
map $http_x_request_id $req_id { default $http_x_request_id; "" $request_id; }
|
||||
log_format json escape=json '{"time":"$time_iso8601","req_id":"$req_id","remote":"$remote_addr","host":"$host","method":"$request_method","uri":"$request_uri","status":$status,"bytes":$bytes_sent,"referer":"$http_referer","ua":"$http_user_agent","up_addr":"$upstream_addr","up_st":"$upstream_status","up_rt":$upstream_response_time,"rt":$request_time}';
|
||||
|
||||
# Log settings
|
||||
access_log /var/log/nginx/access.ndjson json; # structured JSON for ELK
|
||||
access_log /var/log/nginx/access.log; # standard combined for Elastic Agent nginx integration
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# Gzip settings (optional)
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain application/xml text/css application/javascript application/json application/x-javascript text/javascript;
|
||||
|
||||
# Upstream pool for PHP-FPM (load-balanced)
|
||||
upstream php_fpm {
|
||||
least_conn;
|
||||
server php1:9000 max_fails=3 fail_timeout=10s;
|
||||
server php2:9000 max_fails=3 fail_timeout=10s;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# HTTP server (no HTTPS for local development)
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Logging headers
|
||||
add_header X-Request-Id $req_id always;
|
||||
|
||||
# Root and index
|
||||
root /var/www/html;
|
||||
index index.php index.html index.htm;
|
||||
|
||||
# Main application location
|
||||
location / {
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, X-Customer-Number";
|
||||
add_header Access-Control-Allow-Credentials true;
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, X-Customer-Number";
|
||||
return 204;
|
||||
}
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php_fpm;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_read_timeout 60s;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
fastcgi_param X_REQUEST_ID $req_id;
|
||||
fastcgi_keep_conn on;
|
||||
}
|
||||
|
||||
# Deny access to hidden files
|
||||
location ~ /\. { deny all; }
|
||||
location ~ /\.ht { deny all; }
|
||||
}
|
||||
|
||||
# Internal metrics endpoint for Metricbeat (not exposed to host)
|
||||
# Scraped at http://nginx:8080/stub_status by the metricbeat nginx module
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
access_log off;
|
||||
location /stub_status {
|
||||
stub_status;
|
||||
allow all; # safe within the docker network; not published to host
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
-26
@@ -1,6 +1,5 @@
|
||||
# Use the official PHP 8.2 image with Apache disabled
|
||||
FROM php:8.2-fpm
|
||||
|
||||
FROM php:8.2.15-fpm
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /var/www/html
|
||||
|
||||
@@ -9,40 +8,66 @@ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
|
||||
# Install required extensions and dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
libmagickwand-dev \
|
||||
imagemagick \
|
||||
pkg-config \
|
||||
zip \
|
||||
git \
|
||||
curl \
|
||||
libzip-dev \
|
||||
default-mysql-client \
|
||||
&& docker-php-ext-install mbstring exif pcntl bcmath gd pdo_mysql mysqli zip \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick
|
||||
# Install required extensions and dependencies
|
||||
# Install required extensions and dependencies
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
$PHPIZE_DEPS \
|
||||
libpng-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libfreetype6-dev \
|
||||
libxml2-dev \
|
||||
libonig-dev \
|
||||
libmagickwand-dev \
|
||||
libmagickcore-dev \
|
||||
imagemagick \
|
||||
pkg-config \
|
||||
zip \
|
||||
unzip \
|
||||
git \
|
||||
curl \
|
||||
redis-tools \
|
||||
libzip-dev \
|
||||
libssl-dev \
|
||||
ca-certificates \
|
||||
mariadb-client; \
|
||||
update-ca-certificates; \
|
||||
docker-php-ext-configure gd --with-freetype --with-jpeg; \
|
||||
docker-php-ext-install -j"$(nproc)" \
|
||||
mbstring \
|
||||
exif \
|
||||
pcntl \
|
||||
bcmath \
|
||||
gd \
|
||||
pdo_mysql \
|
||||
mysqli \
|
||||
zip; \
|
||||
pecl install imagick-3.7.0; \
|
||||
docker-php-ext-enable imagick; \
|
||||
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $PHPIZE_DEPS; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy application files into the container
|
||||
COPY /nginx/app /var/www/html
|
||||
## Do not copy application code at build time; it will be bind-mounted by docker-compose
|
||||
## If you wish to build a self-contained image, uncomment the next line and adjust the path:
|
||||
## COPY services/nginx/app /var/www/html
|
||||
|
||||
# Set the appropriate permissions for the working directory
|
||||
RUN chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 755 /var/www/html
|
||||
|
||||
# Install using Composer
|
||||
RUN composer install --no-dev
|
||||
|
||||
# Install the /var/www/html/modules/washcertificates directory
|
||||
WORKDIR /var/www/html/modules/washcertificates
|
||||
RUN composer install --no-dev
|
||||
## Composer installs are performed at container start by docker-entrypoint.sh
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
## Ensure workdir is correct
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Copy and enable entrypoint that installs Composer deps on first run
|
||||
COPY services/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Expose port 9000
|
||||
EXPOSE 9000
|
||||
|
||||
# Start PHP-FPM when the container starts
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["php-fpm"]
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Config
|
||||
APP_DIR="/var/www/html"
|
||||
MODULE_DIR="$APP_DIR/modules/washcertificates"
|
||||
LOG_FILE="/var/log/php/composer-install.log"
|
||||
|
||||
# Gate auto-install (set to "true" only on one PHP container, e.g. php1)
|
||||
AUTO_COMPOSER_INSTALL="${AUTO_COMPOSER_INSTALL:-true}"
|
||||
|
||||
log() { printf "[entrypoint] %s\n" "$*"; }
|
||||
|
||||
wait_for_redis() {
|
||||
host="${REDIS_CONFIG_HOST:-redis}"
|
||||
port="${REDIS_CONFIG_PORT:-6379}"
|
||||
pass="${REDIS_CONFIG_PASSWORD:-}"
|
||||
timeout="${REDIS_WAIT_TIMEOUT:-60}"
|
||||
i=0
|
||||
|
||||
log "Waiting for Redis at ${host}:${port} (timeout: ${timeout}s) ..."
|
||||
while [ "$i" -lt "$timeout" ]; do
|
||||
if [ -n "$pass" ]; then
|
||||
if redis-cli -h "$host" -p "$port" -a "$pass" PING >/dev/null 2>&1; then
|
||||
log "Redis is ready"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
if redis-cli -h "$host" -p "$port" PING >/dev/null 2>&1; then
|
||||
log "Redis is ready"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 1; i=$((i+1))
|
||||
done
|
||||
log "ERROR: Timed out waiting for Redis at ${host}:${port}"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_file() {
|
||||
target="$1"; timeout="${2:-120}"; i=0
|
||||
while [ "$i" -lt "$timeout" ]; do
|
||||
if [ -e "$target" ]; then return 0; fi
|
||||
sleep 1; i=$((i+1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
with_install_lock() {
|
||||
# Poor-man's lock using a directory
|
||||
while ! mkdir /tmp/composer-install.lock 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
trap 'rmdir /tmp/composer-install.lock 2>/dev/null || true' EXIT HUP INT TERM
|
||||
"$@"
|
||||
rmdir /tmp/composer-install.lock 2>/dev/null || true
|
||||
trap - EXIT HUP INT TERM
|
||||
}
|
||||
|
||||
install_if_needed() {
|
||||
dir="$1"
|
||||
if [ -f "$dir/composer.json" ]; then
|
||||
if [ ! -f "$dir/vendor/autoload.php" ]; then
|
||||
log "Installing Composer deps in $dir ..."
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
||||
# Run install and log output
|
||||
if ! COMPOSER_ALLOW_SUPERUSER=1 composer install \
|
||||
--no-dev --prefer-dist --optimize-autoloader --no-interaction \
|
||||
-d "$dir" 2>&1 | tee -a "$LOG_FILE"; then
|
||||
log "ERROR: composer install failed in $dir. See $LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
# Verify autoload was created
|
||||
if [ ! -f "$dir/vendor/autoload.php" ]; then
|
||||
log "ERROR: autoload.php still missing after install in $dir. See $LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
# Best-effort permissions fix (ignore errors on non-Linux filesystems)
|
||||
chown -R www-data:www-data "$dir/vendor" 2>/dev/null || true
|
||||
else
|
||||
log "vendor already present in $dir — skipping"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Optionally perform auto-install (only on the designated container)
|
||||
if [ "$AUTO_COMPOSER_INSTALL" = "true" ]; then
|
||||
# Wait for the bind mount and composer.json to appear (common on Windows/macOS)
|
||||
if ! wait_for_file "$APP_DIR/composer.json" 120; then
|
||||
log "WARNING: $APP_DIR/composer.json not found after waiting — skipping auto-install"
|
||||
else
|
||||
with_install_lock install_if_needed "$APP_DIR"
|
||||
fi
|
||||
|
||||
# Module (optional)
|
||||
if [ -f "$MODULE_DIR/composer.json" ]; then
|
||||
with_install_lock install_if_needed "$MODULE_DIR"
|
||||
fi
|
||||
else
|
||||
log "AUTO_COMPOSER_INSTALL=false — skipping Composer auto-install"
|
||||
fi
|
||||
|
||||
# Wait for Redis before starting PHP-FPM (if host is defined)
|
||||
if [ -n "${REDIS_CONFIG_HOST:-}" ]; then
|
||||
wait_for_redis
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,16 @@
|
||||
; PHP custom configuration for sessions and logging
|
||||
|
||||
; Store sessions in Redis so multiple PHP-FPM replicas can share state
|
||||
session.save_handler = redis
|
||||
; Add auth=PASSWORD if you set a password on Redis, and adjust database if needed
|
||||
session.save_path = "tcp://redis:6379?database=0"
|
||||
|
||||
; Logging
|
||||
log_errors = On
|
||||
error_log = /var/log/php/error.log
|
||||
|
||||
; Elastic APM PHP agent
|
||||
; The extension is installed via apt (elastic-apm-php)
|
||||
extension=elastic_apm.so
|
||||
; Optional bootstrap (enables automatic instrumentation where supported)
|
||||
;elastic_apm.bootstrap_php_part_file=/opt/elastic/apm-agent-php/src/bootstrap_php_part.php
|
||||
@@ -0,0 +1,102 @@
|
||||
http:
|
||||
routers:
|
||||
# Nextcloud proxy (directly from Traefik to external server)
|
||||
cloud:
|
||||
rule: Host(`cloud.truckwash.dk`)
|
||||
entryPoints: [websecure]
|
||||
service: cloud-svc
|
||||
middlewares: [secure-headers]
|
||||
tls:
|
||||
certResolver: le
|
||||
cloud-http:
|
||||
rule: Host(`cloud.truckwash.dk`)
|
||||
entryPoints: [web]
|
||||
middlewares: [redirect-to-https, secure-headers]
|
||||
service: cloud-svc
|
||||
|
||||
# CORS preflight handlers (Traefik generates the OPTIONS response)
|
||||
# Return an empty 204 from Traefik itself and attach CORS headers via the middleware.
|
||||
api-preflight:
|
||||
rule: Host(`api.truckwash.dk`) && Method(`OPTIONS`)
|
||||
entryPoints: [websecure]
|
||||
middlewares: [secure-headers]
|
||||
service: noop@internal
|
||||
priority: 1000
|
||||
tls:
|
||||
certResolver: le
|
||||
|
||||
cloud-preflight:
|
||||
rule: Host(`cloud.truckwash.dk`) && Method(`OPTIONS`)
|
||||
entryPoints: [websecure]
|
||||
middlewares: [secure-headers]
|
||||
service: noop@internal
|
||||
priority: 1000
|
||||
tls:
|
||||
certResolver: le
|
||||
|
||||
# Dashboard routers are declared via labels; middlewares are defined below.
|
||||
|
||||
middlewares:
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
dashboard-auth:
|
||||
basicAuth:
|
||||
# IMPORTANT: Replace REPLACE_WITH_BCRYPT_HASH with a bcrypt/htpasswd hash for user 'truckwash'.
|
||||
# Example command to generate (once Docker is available):
|
||||
# docker run --rm httpd:2.4-alpine htpasswd -nbB truckwash 'w5j9MlEVz62Exm6r'
|
||||
users:
|
||||
- "truckwash:$2y$05$DYcMFqMPgEFWAJQioc.F3.v9ppi9bReAi/aQzoOlWqMyhIysKlDCC"
|
||||
dashboard-allow-local:
|
||||
ipWhiteList:
|
||||
sourceRange:
|
||||
- 127.0.0.1/32
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
secure-headers:
|
||||
headers:
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
customFrameOptionsValue: "SAMEORIGIN"
|
||||
permissionsPolicy: "geolocation=(), microphone=(), camera=(), payment=()"
|
||||
addVaryHeader: true
|
||||
accessControlAllowCredentials: true
|
||||
accessControlMaxAge: 86400
|
||||
accessControlAllowOriginList:
|
||||
- "https://truckwash.io"
|
||||
accessControlAllowMethods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
accessControlAllowHeaders:
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- X-Customer-Number
|
||||
api-ratelimit:
|
||||
rateLimit:
|
||||
average: 100
|
||||
burst: 200
|
||||
|
||||
services:
|
||||
cloud-svc:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://94.130.142.41:11000"
|
||||
|
||||
tls:
|
||||
options:
|
||||
default:
|
||||
minVersion: VersionTLS12
|
||||
sniStrict: true
|
||||
cipherSuites:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||||
@@ -0,0 +1,28 @@
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
|
||||
providers:
|
||||
docker:
|
||||
exposedByDefault: false
|
||||
file:
|
||||
filename: /etc/traefik/dynamic.yml
|
||||
watch: true
|
||||
|
||||
api:
|
||||
dashboard: true
|
||||
|
||||
log:
|
||||
level: WARN
|
||||
|
||||
accessLog: {}
|
||||
|
||||
certificatesResolvers:
|
||||
le:
|
||||
acme:
|
||||
email: jb@truckwash.dk
|
||||
storage: /acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
@@ -0,0 +1,49 @@
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
metrics:
|
||||
address: ":9100"
|
||||
|
||||
providers:
|
||||
docker:
|
||||
exposedByDefault: false
|
||||
file:
|
||||
filename: /etc/traefik/dynamic.yml
|
||||
watch: true
|
||||
|
||||
api:
|
||||
dashboard: true
|
||||
|
||||
log:
|
||||
level: INFO
|
||||
|
||||
accessLog: {}
|
||||
|
||||
metrics:
|
||||
prometheus:
|
||||
entryPoint: metrics
|
||||
addEntryPointsLabels: true
|
||||
addRoutersLabels: true
|
||||
addServicesLabels: true
|
||||
|
||||
tracing:
|
||||
serviceName: traefik
|
||||
jaeger:
|
||||
samplingServerURL: http://jaeger:5778/sampling
|
||||
samplingType: const
|
||||
samplingParam: 1.0
|
||||
localAgentHostPort: jaeger:6831
|
||||
traceContextHeaderName: uber-trace-id
|
||||
|
||||
certificatesResolvers:
|
||||
le:
|
||||
acme:
|
||||
email: jb@truckwash.dk
|
||||
storage: /acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
# Using Let's Encrypt STAGING for local runs to avoid rate limits.
|
||||
# Remove the following caServer line to switch to production.
|
||||
caServer: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
@@ -8,6 +8,105 @@ Content-Type: application/json
|
||||
"country_code": 45
|
||||
}
|
||||
|
||||
### POST request to /subusers
|
||||
POST https://api.truckwash.dk:4433/subusers
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8
|
||||
|
||||
{
|
||||
"cvr": 41004355,
|
||||
"phone_country_code": 45,
|
||||
"phone": 42331128
|
||||
}
|
||||
|
||||
### GET request to /subusers/setup
|
||||
GET https://api.truckwash.dk:4433/subusers/setup?token=1c1be8280bac3937487e5c77b76bb839
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8
|
||||
|
||||
|
||||
### POST request to /subusers/setup
|
||||
POST https://api.truckwash.dk:4433/subusers/setup
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8
|
||||
|
||||
{
|
||||
"token": "1c1be8280bac3937487e5c77b76bb839",
|
||||
"name": "Test Subuser",
|
||||
"username": "testsubuser",
|
||||
"email": "jb@truckwash.dk",
|
||||
"password": "Test1234"
|
||||
}
|
||||
|
||||
### POST request to /subusers/auth/password
|
||||
POST https://api.truckwash.dk:4433/subusers/auth/password
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testsubuser",
|
||||
"password": "Test1234"
|
||||
}
|
||||
|
||||
### GET request to /order-bookings (As subuser)
|
||||
GET https://api.truckwash.dk:4433/order-bookings
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer 9ed711260d58165ee7601060bf7463f6e201481edd16c8ee4fc4a5f04b79a232
|
||||
X-Customer-Number: 42331128
|
||||
|
||||
### GET request to /order-bookings (As subuser, specific ID)
|
||||
GET https://api.truckwash.dk:4433/order-bookings?id=76
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer 9ed711260d58165ee7601060bf7463f6e201481edd16c8ee4fc4a5f04b79a232
|
||||
X-Customer-Number: 42331128
|
||||
|
||||
### GET request to /vehicles (As subuser)
|
||||
GET https://api.truckwash.dk:4433/vehicles
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer 9ed711260d58165ee7601060bf7463f6e201481edd16c8ee4fc4a5f04b79a232
|
||||
X-Customer-Number: 42331128
|
||||
|
||||
### GET request to /vehicles (As subuser, specific ID)
|
||||
GET https://api.truckwash.dk:4433/vehicles?id=384
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer 9ed711260d58165ee7601060bf7463f6e201481edd16c8ee4fc4a5f04b79a232
|
||||
X-Customer-Number: 42331128
|
||||
|
||||
### GET request to /orders (As subuser)
|
||||
GET https://api.truckwash.dk:4433/orders
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer 9ed711260d58165ee7601060bf7463f6e201481edd16c8ee4fc4a5f04b79a232
|
||||
X-Customer-Number: 42331128
|
||||
|
||||
|
||||
### GET request to /subusers/me (As subuser)
|
||||
GET https://api.truckwash.dk:4433/subusers/me
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer 9ed711260d58165ee7601060bf7463f6e201481edd16c8ee4fc4a5f04b79a232
|
||||
X-Customer-Number: 42331128
|
||||
|
||||
### GET request to /subusers/permission-nodes
|
||||
GET https://api.truckwash.dk:4433/subusers/permission-nodes
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8
|
||||
|
||||
### GET request to /subusers/grants (As subuser)
|
||||
GET https://api.truckwash.dk:4433/subusers/grants
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer 9ed711260d58165ee7601060bf7463f6e201481edd16c8ee4fc4a5f04b79a232
|
||||
X-Customer-Number: 42331128
|
||||
|
||||
### GET request to order bookings (list all)
|
||||
GET https://api.truckwash.dk:4433/order-bookings
|
||||
Accept: application/json
|
||||
|
||||
Reference in New Issue
Block a user