Since I rarely give insights on how websites such as this one are run, I decided it would be a great time to share one very simplistic and efficient approach to host several small websites, separated from each other, on a cheap VPS.
When we talk websites, I mean WordPress instances. WordPress has a few basic requirements:
- the PHP scripting-language
- a MySQL database
- possiblity to use sendmail or a similar software to send mails
To achieve these basic requirements, I employ:
- one docker-compose stack bundling the MySQL-database as well as the phpMyAdmin-webinterface
- multiple docker-compose stacks bundling PHP-FPM, nginx as well as exim4 to act as a mailrelay
- one nginx instance on the VPS (uncontainered as of now) that does the SSL-offloading and acts as a reverse proxy in front of the different docker-compose stacks for WordPress
The setup is:
- simple
- easy to handle (upgrades, PHP-version switching etc.)
- efficient (especially towards RAM usage)
- easily migrateable to a different machine
Database stack:
version: '3'
services:
db:
image: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: safepw
volumes:
- "/var/lib/mysql:/var/lib/mysql"
ports:
- "3306:3306"
pma:
image: phpmyadmin/phpmyadmin
environment:
- PMA_HOST=myadress
- UPLOAD_LIMIT=256M
- PMA_ABSOLUTE_URI="https://myadress/db/"
- MAX_EXECUTION_TIME="600"
restart: always
volumes:
- /sessions
- /var/www/pma/config.user.inc.php:/etc/phpmyadmin/config.user.inc.php
ports:
- "127.0.0.1:9501:80"
WordPress stack:
version: '3'
services:
fpm:
build: ../wordpress/fpm-alpine-8/
restart: always
depends_on:
- "mailrelay"
environment:
- "MAILDOMAIN=mailhostname"
- "MAILRELAY=mailrelay"
- "WORDPRESS_DB_HOST=dbhostname"
- "WORDPRESS_DB_USER=website.org"
- "WORDPRESS_DB_PASSWORD=safepw"
- "WORDPRESS_DB_NAME=website.org"
volumes:
- "/var/www/website.org/var/www/html"
- "/var/www/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini"
nginx:
image: 'nginx:latest'
restart: always
ports:
- '127.0.0.1:9100:80'
volumes:
- "/var/www/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro"
- "/var/www/nginx/nginx.conf:/etc/nginx/nginx.conf:ro"
- "/var/www/website.org:/var/www/html"
mailrelay:
image: "namshi/smtp"
restart: always
environment:
- "MAILNAME=myadress"
The Dockerfile referenced in the docker-compose.yml of the WordPress-stack:
FROM wordpress:php8.0-fpm-alpine
RUN apk add --no-cache msmtp; \
echo 'sendmail_path = "/usr/bin/msmtp -t"' > /usr/local/etc/php/conf.d/msmtp.ini
COPY ./docker-entrypoint-pre.sh /usr/local/bin/docker-entrypoint-pre.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint-pre.sh"]
CMD ["php-fpm"]
And the docker-entrypoint-pre.sh referenced in the Dockerfile above:
#!/bin/bash
echo "account default
host ${MAILRELAY}
auto_from on
maildomain ${MAILDOMAIN}" > /etc/msmtprc
exec /usr/local/bin/docker-entrypoint.sh "$@"
The referenced nginx-config default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
rewrite /wp-admin$ $scheme://$host$request_uri/ permanent;
client_max_body_size 4096M;
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
fastcgi_pass fpm:9000;
fastcgi_index index.php;
}
}
The referenced nginx-config nginx.conf:
user nginx;
worker_processes 4;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
fastcgi_read_timeout 120s;
fastcgi_send_timeout 120s;
}
Nginx reverse proxy and SSL-offloading config (example, I recommend you do your own fitting config):
upstream website.org {
server 127.0.0.1:9100;
}
server {
listen 443 http2 ssl;
listen [::]:443 http2 ssl;
server_name website.org;
access_log /dev/null;
error_log /dev/null;
client_max_body_size 16M;
ssl_certificate /etc/letsencrypt/live/www.website.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.website.org/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_dhparam /etc/nginx/ssl/website.org/dhparam.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:EECDH:EDH:!MD5:!RC4:!LOW:!MEDIUM:!CAMELLIA:!ECDSA:!DES:!DSS:!3DES:!NULL;
ssl_prefer_server_ciphers on;
gzip on;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
location / {
proxy_pass http://website.org;
proxy_set_header X-Forwarded-Proto $scheme;
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 6M;
add_header Cache-Control "public";
proxy_pass http://website.org;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~* \.(?:css|js|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public";
proxy_pass http://website.org;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name www.website.org;
return 301 $scheme://website.org$request_uri;
ssl_certificate /etc/letsencrypt/live/www.website.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.website.org/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_dhparam /etc/nginx/ssl/website.org/dhparam.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:EECDH:EDH:!MD5:!RC4:!LOW:!MEDIUM:!CAMELLIA:!ECDSA:!DES:!DSS:!3DES:!NULL;
ssl_prefer_server_ciphers on;
}
server {
listen 80;
listen [::]:80;
server_name website.org;
return 301 https://website.org$request_uri;
}