Guide to install Nginx + Php + MariaDB + Phpmyadmin in Docker (XNMP-VHOST)
Every time I have a new computer whether it be a MacBook or PC (PopOS). I have to install MAMP / XAMP with some brew installations, without a clue where all those files being installed. Sometimes you want to run different PHP version depending on a project or perhaps you want to revive legacy ones, run https SSL locally because WebRTC only works secured environment or even attach a Node project with socket file.
And you don't want to pollute your machine with development files unless containerized.
Docker is my man!
For this guide I let you install some Docker containers, working with Nginx configurations, connect containers, run docker commands, have SSL Certificates on your localhost.
- Alpine
- NGINX
- PHP FPM 5.6.23 / 7.0.8 / 8.0.0
- MariaDB (MySQL)
- Phpmyadmin
Also:
- PHP FPM with extra modules
- Self-signed Certificates for localhost
- Letsencrypt automation
- You can skip the whole article and scroll down to download from my github repository.Article Updates
- This article expects you already have Docker installed on your machine and haverootprivileges and know some terminal commands. This article is base on a PopOS/Debian computer.
- Tip: Post-installation steps for Linux to rundockerwithout sudo
- 2021-1-29 # Changed File Structure build and data
- 2021-1-1 #
Beter naming convention;
Letsencrypt and automation - 2020-12-19 #
Dockerfile PHP 8 FPM - 2020-12-18 #
Improved docker-compose.yml for database and phpmyadmin
Let’s create some files
Create docker-compose.yml
version: '2'
services:
nginx:
image: nginx:alpine
restart: always
links:
- 5-6-23-fpm
- 7-0-8-fpm
- 8-0-0-fpm
ports:
- "80:80"
- "443:443"
volumes:
- ./data/nginx/enabled:/etc/nginx/conf.d
- ./data/nginx/snippets:/nginx/snippets
- ./data/nginx/certificates:/nginx/certificates
volumes_from:
- data
5-6-23-fpm:
image: php:5.6.23-fpm-alpine
restart: always
volumes_from:
- data
7-0-8-fpm:
image: php:7.0.8-fpm-alpine
restart: always
volumes_from:
- data
8-0-0-fpm:
image: php:8.0.0-fpm-alpine
restart: always
volumes_from:
- data
db:
image: madiadb #v10.5.8
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./data/db:/var/lib/mysql
dbadmin:
image: phpmyadmin/phpmyadmin
restart: always
environment:
PMA_HOST: db
PMA_USER: root # Remove line for production
PMA_PASSWORD: root # Remove line for production
depends_on:
- db
# ports:
# - "8080:80"
data:
image: alpine:latest
command: echo "READY!!!"
volumes:
- ./data/vhosts:/vhosts
- ./data/tmp:/tmp/var/docker/docker-xnmp-vhosts/docker-compose.ymlThen run command from /var/docker/docker-xnmp-vhosts/ directory.
# Start docker containers from compose file
docker-compose up/var/docker/docker-xnmp-vhosts/You can also run this as background process: docker-compose up -dFrom here if you check volumes some directories are created, and currently the port 80 and 443 are available from localhost. http://localhost
Upon visiting the url, there's actually nothing to see except an 404 error page
Create simple page for Default vhosts
<!DOCTYPE html>
<html>
<head>
<title>DEFAULT DOMAIN</title>
<style>
body {
background: black;
color: hotpink;
}
</style>
</head>
<body>
This is default domain
</body>
</html>/var/docker/docker-xnmp-vhosts/data/vhosts/_default_/httpdocs/index.htmlCreate Nginx Default configuration
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
index index.html index.htm;
root /vhosts/_default_/httpdocs;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ /index.php$is_args$args =404;
}
}
/var/docker/docker-xnmp-vhosts/data/nginx/enabled/_default_.confRestart NGINX
Sinds we know our docker containers are still running, we can run the command from/var/docker/docker-xnmp-vhosts/ directory:
# Restart Nginx
docker-compose exec nginx nginx -t && docker-compose restart nginx/var/docker/docker-xnmp-vhosts/This command will use already running nginx container and execute a commandnginx -tfor testing valid configuration and restart only nginx container.
nginx - is the service name we gave in the docker-compose.yml file.
nginx -t - is the command that are available in the nginx container.
Every changes you make in the .conf file. You need to restart your nginx server to take effect.
After web server restart we can visit the page: http://localhost
Virtual Hosts (VHOSTS)
Let’s create a file structure for example.com with two sub-domains in mind for example: antique and beauty.
/var/docker/docker-xnmp-vhosts/
data/
nginx/
enabled/example.com.conf
vhosts/
example.com/
httpdocs/index.html
subdomains/
antique/httpdocs/index.html
beauty/httpdocs/index.htmlAfter good configuration, you can add as many subdomains as you wish without the need to restart Nginx
Vhost: example.com
Virtual #1 - example.com
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
<style>
body {
background: lightgrey;
color: cadetblue;
}
</style>
</head>
<body>
Example domain
</body>
</html>/var/docker/docker-xnmp-vhosts/data/vhosts/example.com/httpdocs/index.htmlVirtual #2 - antique.example.com
<!DOCTYPE html>
<html>
<head>
<title>Antique Example</title>
<style>
body {
background: lightslategrey;
color: lightblue;
}
</style>
</head>
<body>
Antique subdomain
</body>
</html>/var/docker/docker-xnmp-vhosts/data/vhosts/example.com/subdomains/antique/httpdocs/index.htmlVirtual #3 - beauty.example.com
<!DOCTYPE html>
<html>
<head>
<title>Beauty Example</title>
<style>
body {
background: lightslategrey;
color: lightblue;
}
</style>
</head>
<body>
Beauty subdomain
</body>
</html>/var/docker/docker-xnmp-vhosts/data/vhosts/example.com/subdomains/beauty/httpdocs/index.htmlNginx configuration: example.com
# domain: example.com
server {
disable_symlinks off;
server_name ~^example\.com(\.localhost)?$;
root /vhosts/example.com/httpdocs;
autoindex on;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string =404;
}
}
# subdomains: *.example.com
server {
disable_symlinks off;
server_name ~^((?<subdomain>.*)\.)example\.com(\.localhost)?$;
root /vhosts/example.com/subdomains/${subdomain}/httpdocs;
autoindex on;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string =404;
}
}
/var/docker/docker-xnmp-vhosts/data/nginx/enabled/example.com.confNginx advanced configuration that takes regular expression-like.
To make*wildcard work, subdomain will be used for${subdomain}
Restart Nginx to apply new configurations and visit to see:
- example.com.localhost
- antique.example.com.localhost
- beauty.example.com.localhost
Configuring NGINX for PHP Projects
/var/docker/docker-xnmp-vhosts/
data/
nginx/
enabled/
php5.conf
php7.conf
php8.conf
snippets/
php-5.6.23-fpm.conf
php-7.0.8-fpm.conf
php-8.0.0-fpm.conf
vhosts/
php5/httpdocs/index.php
php7/httpdocs/index.php
php8/httpdocs/index.phpPHP 5 & 7 & 8
Create simple phpinfo files in the vhosts directory
<?php phpinfo()index.phpfile:/var/docker/docker-xnmp-vhosts/data/vhosts/php5/httpdocs/index.php
file:/var/docker/docker-xnmp-vhosts/data/vhosts/php7/httpdocs/index.php
file:/var/docker/docker-xnmp-vhosts/data/vhosts/php8/httpdocs/index.php
Create reusable snippets
#location ~ \.php(/|$) {
try_files $uri = 404;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS on;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
#}/var/docker/docker-xnmp-vhosts/data/nginx/snippets/snippet-php-fastcgi.conflocation ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 5-6-23-fpm:9000;
}/var/docker/docker-xnmp-vhosts/data/nginx/snippets/php-5.6.23-fpm.conflocation ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 7-0-8-fpm:9000;
}/var/docker/docker-xnmp-vhosts/data/nginx/snippets/php-7.0.8-fpm.conflocation ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 8-0-0-fpm:9000;
}/var/docker/docker-xnmp-vhosts/data/nginx/snippets/php8.0.0-fpm.confApply reusable snippets for domains
server {
disable_symlinks off;
server_name ~^php5(\.localhost)?$;
root /vhosts/php5/httpdocs;
autoindex on;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
include /nginx/snippets/php-5.6.23-fpm.conf;
}/var/docker/docker-xnmp-vhosts/data/nginx/enabled/php5.confserver {
disable_symlinks off;
server_name ~^php7(\.localhost)?$;
root /vhosts/php7/httpdocs;
autoindex on;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
include /nginx/snippets/php-7.0.8-fpm.conf;
}/var/docker/docker-xnmp-vhosts/data/nginx/enabled/php7.confserver {
disable_symlinks off;
server_name ~^php8(\.localhost)?$;
root /vhosts/php8/httpdocs;
autoindex on;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
include /nginx/snippets/php-8.0.0-fpm.conf;
}/var/docker/docker-xnmp-vhosts/data/nginx/enabled/php8.confRestart Nginx to apply new configurations and visit to see:
- php5.localhost
- php7.localhost
- php8.localhost
Revive legacy projects with PHP extensions
When you found out that your old website uses Mcrypt, your page shows PHP errors and simple docker container isn’t enough that would force you to install some extensions.
/var/docker/docker-xnmp-vhosts/
docker-compose.yml
build/7-0-8-fpm-ext/
Dockerfile
data/
nginx/
enabled/php7-ext.conf
snippets/php-7.0.8-fpm-ext.conf
vhosts/
php7-ext/httpdocs/index.phpAdd legacy project in Virtual Host directory
<?php phpinfo()/var/docker/docker-xnmp-vhosts/data/vhosts/php7-ext/httpdocs/index.phpCreate Nginx snippet and domain config
location ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 7-0-8-fpm-ext:9000;
}/var/docker/docker-xnmp-vhosts/data/nginx/snippets/php-7.0.8-fpm-ext.confserver {
disable_symlinks off;
server_name ~^php7-ext(\.localhost)?$;
root /vhosts/php7-ext/httpdocs;
autoindex on;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
include /nginx/snippets/php-7.0.8-fpm-ext.conf;
}/var/docker/docker-xnmp-vhosts/data/nginx/enabled/php7-ext.confCreate custom Dockerfile with php extensions
Luckily you can create your own Dockerfile and use that in your docker-compose.yml.
PHP 7.0.8 FPM with Extensions
FROM php:7.0.8-fpm-alpine
RUN apk add --no-cache --update \
libmcrypt \
libmcrypt-dev \
&& docker-php-ext-install \
mysqli \
opcache \
intl \
sockets \
mcrypt \
&& rm -rf /tmp/* /var/cache/apk/* \
&& echo "=============================================" \
&& php -m
/var/docker/docker-xnmp-vhosts/build/7-0-8-fpm-ext/Dockerfilephp:7.0.8-fpm-alpine is docker image it build from. You can choose whichever version you wish, but the installation script may change, for example mcrypt on version 7.1.x.PHP 8.0.0 FPM with Extensions
FROM php:8.0.0-fpm-alpine
######## [PHP Modules] Default ########
#### Core
#### ctype
#### curl
#### date
#### dom
#### fileinfo
#### filter
#### ftp
#### hash
#### iconv
#### json
#### libxml
#### mbstring
#### mysqlnd
#### openssl
#### pcre
#### PDO
#### pdo_sqlite
#### Phar
#### posix
#### readline
#### Reflection
#### session
#### SimpleXML
#### sodium
#### SPL
#### sqlite3
#### standard
#### tokenizer
#### xml
#### xmlreader
#### xmlwriter
#### zlib
######## Composer.phar ########
RUN curl -s https://getcomposer.org/installer | php \
# move composer into a bin directory you control:
&& mv composer.phar /usr/local/bin/composer \
# double check composer works
&& composer about
RUN php -m && echo "============================================="
######## Dependencies ########
#### bzip2-dev: bz2
#### enchant2-dev: enchant
#### gd: libpng-dev
#### gmp: gmp-dev
#### imap: imap-dev
#### intl: icu-dev
#### ldap: openldap-dev
#### pdo_dblib: freetds-dev
#### pdo_pgsql: postgresql-dev
#### pgsql: postgresql-dev
#### pspell: aspell-dev
#### snmp: net-snmp-dev
#### soap: libxml2-dev
#### tidy: tidyhtml-dev
#### xsl: libxslt-dev
#### zip: libzip-dev
RUN apk add --no-cache --update \
bzip2-dev \
enchant2-dev \
libpng-dev \
gmp-dev \
imap-dev \
icu-dev \
openldap-dev \
freetds-dev \
postgresql-dev \
aspell-dev \
net-snmp-dev \
libxml2-dev \
tidyhtml-dev \
libxslt-dev \
libzip-dev
RUN docker-php-ext-install \
bcmath \
bz2 \
calendar \
dba \
enchant \
exif \
ffi \
gd \
gettext \
gmp \
imap \
intl \
ldap \
mysqli \
opcache \
pcntl \
pdo_dblib \
pdo_mysql \
pdo_pgsql \
pgsql \
pspell \
shmop \
snmp \
soap \
sockets \
sysvmsg \
sysvsem \
sysvshm \
tidy \
xsl \
zend_test \
zip
######## PHP MODULES not working yet ########
#### oci8
#### odbc
#### pdo_firebird
#### pdo_oci
#### pdo_odbc
RUN rm -rf /tmp/* /var/cache/apk/* \
&& echo "=============================================" \
&& php -m
/var/docker/docker-xnmp-vhosts/build/8-0-0-fpm-ext/DockerfileAlready installed PHP modules from php:8.0.0-fpm-alpine.[PHP Modules]
Core
ctype
curl
date
dom
fileinfo
filter
ftp
hash
iconv
json
libxml
mbstring
mysqlnd
openssl
pcre
PDO
pdo_sqlite
Phar
posix
readline
Reflection
session
SimpleXML
sodium
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
zlibPHP MODULES not yet installed: oci8 odbc pdo_firebird pdo_oci pdo_odbc
Apply PHP with extension Dockerfile in the docker-compose.yml.
Add new service: 7-0-8-fpm-ext
services:
7-0-8-fpm-ext:
build: build/7-0-8-fpm-ext
restart: always
volumes_from:
- datadocker-compose.ymlSnippet
Add new service: 8-0-0-fpm-ext
services:
8-0-0-fpm-ext:
build: build/8-0-0-fpm-ext
restart: always
volumes_from:
- datadocker-compose.ymlSnippet
Link Nginx with the new service
services:
nginx:
links:
- 7-0-8-fpm-ext
- 8-0-0-fpm-extdocker-compose.ymlSnippet
Full configuration
version: '2'
services:
nginx:
image: nginx:alpine
restart: always
links:
- 5-6-23-fpm
- 7-0-8-fpm
- 7-0-8-fpm-ext
- 8-0-0-fpm
- 8-0-0-fpm-ext
ports:
- "80:80"
- "443:443"
volumes:
- ./data/nginx/enabled:/etc/nginx/conf.d
- ./data/nginx/snippets:/nginx/snippets
volumes_from:
- data
5-6-23-fpm:
image: php:5.6.23-fpm-alpine
restart: always
volumes_from:
- data
7-0-8-fpm:
image: php:7.0.8-fpm-alpine
restart: always
volumes_from:
- data
7-0-8-fpm-ext:
build: build/7-0-8-fpm-ext
restart: always
volumes_from:
- data
8-0-0-fpm:
image: php:8.0.0-fpm-alpine
restart: always
volumes_from:
- data
8-0-0-fpm-ext:
build: build/8-0-0-fpm-ext
restart: always
volumes_from:
- data
db:
image: mariadb #v10.5.8
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./data/db:/var/lib/mysql
dbadmin:
image: phpmyadmin/phpmyadmin
restart: always
environment:
PMA_HOST: db
PMA_USER: root # Remove line for production
PMA_PASSWORD: root # Remove line for production
depends_on:
- db
data:
image: alpine:latest
command: echo "--- Docker data volume READY."
volumes:
- ./data/vhosts:/vhosts
- ./data/tmp:/tmp/var/docker/docker-xnmp-vhosts/docker-compose.ymlThis time we only need to full restart the docker containers to take effect.
If you have a running terminal and you used: docker-compose up you can press:
Ctrl+C to close program
# Check running containers
docker-compose ps
# Kill containers when you use: docker-compose up -d
docker-compose down
# Start containers as background
docker-compose up -d/var/docker/docker-xnmp-vhosts/Command options
Create symbolic link (aka symlink) to nginx config file
A symbolic link for example to the latest version php configuration file.
/var/docker/docker-xnmp-vhosts/data/
nginx/
snippets/php-fpm-default.confFor example: ln -nsf <source> <target>
# Create php-fpm-default.conf symlink for later use
ln -nsf php-8.0.0-fpm-ext.conf php-fpm-default.conf/var/docker/docker-xnmp-vhosts/nginx/snippets/Manage MySQL Database with dockerized Phpmyadmin
Since we already have a mysql-database and phpmyadmin-webapp in our docker-compose.yml. We only need to config Nginx and point to it.
/var/docker/docker-xnmp-vhosts/
data/
nginx/
enabled/dbadmin.conf
snippets/snippet-server-location-upstream.confNginx configuration for Phpmyadmin
# server {
# server_name ~^dbadmin(\.localhost)?$;
# resolver 127.0.0.11 valid=30s;
# set $upstream http://dbadmin:80;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass $upstream;
}
# }/var/docker/docker-xnmp-vhosts/nginx/snippets/snippet-server-location-upstream.confserver {
server_name ~^dbadmin(\.localhost)?$;
resolver 127.0.0.11 valid=30s;
set $upstream http://dbadmin:80;
include /nginx/snippets/snippet-server-location-upstream.conf;
}
/var/docker/docker-xnmp-vhosts/nginx/enabled/dbadmin.confRestart Nginx to apply new configurations and visit to see:
- dbadmin.localhost
host: db, username: root, password: root
To change database root password editdocker-compose.ymland look for MYSQL_ROOT_PASSWORD
Configure Self-signed Certificates SSL for development
/var/docker/docker-xnmp-vhosts/
data/
nginx/
bin/
createDomainDirectory.sh
createLocalhost.sh
createNginxSslConfigFileExample.sh
createRootCA.sh
dns.txt.example
recreateNginxSslConfigFileExample.sh
README.md
enabled/ssl.conf
snippets/
snippet-ssl.conf
ssl-defaultserver.conf
ssl-domain.conf
vhosts/
ssl/
httpdocs/index.php
subdomains/
test/httpdocs/index.phpPost-install SSL for Debian/Ubuntu
When you run command (when you followed and finish this SSL article) curl https://ssl.localhost would work perfectly fine when in you install certificates on your system, but visiting that URL in Chrome will not. You’ll get security error page. On Mac wouldn't have that problem because those browsers uses the system Trust Store.
You can tell applications (such as Chrome / Firefox) that use NSS for its certificate management to use the system Trust Store.
sudo apt-get update && sudo apt-get install -y p11-kit libnss3
find / -type f -name "libnssckbi.so" 2>/dev/null | while read line; do
sudo mv $line ${line}.bak
sudo ln -s /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so $line
donelibnssckbi.so out there than just in libnss3. The following is a script to find them all, back them up, and replace them with links to p11-kitFound this script from Superuser
Guide to add self-generated root certificate authorities for 8 operating systems and browsers
Create SSL simple pages for vhosts
<?php phpinfo()/var/docker/docker-xnmp-vhosts/data/vhosts/ssl/httpdocs/index.phpfile:/var/docker/docker-xnmp-vhosts/data/vhosts/ssl/httpdocs/index.php
file:/var/docker/docker-xnmp-vhosts/data/vhosts/ssl/subdomains/test/httpdocs/index.php
Create SSL Snippets for Nginx
# ssl on;
ssl_certificate /nginx/certificates/localhost/localhost.crt;
ssl_certificate_key /nginx/certificates/localhost/localhost.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA';
ssl_prefer_server_ciphers on;
/var/docker/docker-xnmp-vhosts/data/nginx/snippets/snippet-ssl.conflisten 443 ssl default_server;
listen [::]:443 ssl default_server;
include /nginx/snippets/snippet-ssl.conf;
/var/docker/docker-xnmp-vhosts/data/nginx/snippets/ssl-defaultserver.conflisten 443 ssl;
listen [::]:443 ssl;
include /nginx/snippets/snippet-ssl.conf;
/var/docker/docker-xnmp-vhosts/data/nginx/snippets/ssl-domain.confOptional: ssl.conf
This Nginx config file will be generated by the bin script
server {
server_name ~^ssl(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain.conf;
server_name ~^ssl(\.localhost)?$;
index index.html index.php;
root /vhosts/ssl/httpdocs;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
include /nginx/snippets/php-fpm-default.conf;
}
# SUBDOMAINS
server {
server_name ~^((?<subdomain>.*)\.)ssl(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain.conf;
server_name ~^((?<subdomain>.*)\.)ssl(\.localhost)?$;
index index.html index.php;
root /vhosts/ssl/subdomains/${subdomain}/httpdocs;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
include /nginx/snippets/php-fpm-default.conf;
}/var/docker/docker-xnmp-vhosts/data/nginx/enabled/ssl.confScripts to create SSL Certificates for localhost development
Skip this part if you want to download script files and run single command.
dns.txt
localhost
*.localhost
*.example.com.localhost
example.com.localhost
php5.localhost
*.php5.localhost
php7.localhost
*.php7.localhost
php7-ext.localhost
*.php7-ext.localhost
php8.localhost
*.php8.localhost
php8-ext.localhost
*.php8-ext.localhost
ssl.localhost
*.ssl.localhost/var/docker/docker-xnmp-vhosts/data/nginx/bin/dns.txtcreateDomainDirectory.sh
#!/bin/bash
CERT_DIR=../certificates
if [[ $# -eq 0 ]] ; then
echo -e 'Error Script: Need argument\n\n\tEXAMPLE: ./createDomainDirectory.sh localhost'
exit 0
fi
OUTPUT=`cat <<EOF
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
IP.1 = 10.10.10.20
IP.2 = 127.0.0.1
EOF
`
# OUTPUT: EOF
# Import dns.txt file if exist, else use dns.txt.example and make newline as a array and sort
DNSTXT=dns.txt
[ ! -f "$DNSTXT" ] && DNSTXT=dns.txt.example
IFS=$'\r\n' GLOBIGNORE='*' command eval 'DOMAINS=($(sort <"$DNSTXT"))'; unset IFS
for DNS in "${DOMAINS[@]}"
{
(( COUNT++ ))
OUTPUT="${OUTPUT}\nDNS.$COUNT = $DNS"
}
echo -e "$OUTPUT"
echo -e "$OUTPUT" > domains.ext
# Create Dir
mkdir -p $CERT_DIR/$1
# Generate Certificates and Keys for Domain
openssl req -new -nodes -newkey rsa:2048 -keyout $CERT_DIR/$1/$1.key -out $CERT_DIR/$1/$1.csr -subj "/CN=localhost"
openssl x509 -req -sha256 -days 1024 -in $CERT_DIR/$1/$1.csr -CA $CERT_DIR/RootCA.pem -CAkey $CERT_DIR/RootCA.key -CAcreateserial -extfile domains.ext -out $CERT_DIR/$1/$1.crt/var/docker/docker-xnmp-vhosts/data/nginx/bin/createDomainDirectory.shcreateLocalhost.sh
#!/bin/bash
./createDomainDirectory.sh localhost
/var/docker/docker-xnmp-vhosts/data/nginx/bin/createLocalhost.shcreateRootCA.sh
#!/bin/bash
CERT_DIR=../certificates
openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout $CERT_DIR/RootCA.key -out $CERT_DIR/RootCA.pem -subj "/C=NL/O=XNMP/CN=XNMP-Root-CA"
openssl x509 -outform pem -in $CERT_DIR/RootCA.pem -out $CERT_DIR/RootCA.crt
COMMAND_CP_CA_CERTIFICATES_TO_SYSTEM="cp $CERT_DIR/RootCA.crt /usr/local/share/ca-certificates && update-ca-certificates -f"
if [[ -d /usr/local/share/ca-certificates ]]; then
[[ $EUID -ne 0 ]] && sudo bash -c "$COMMAND_CP_CA_CERTIFICATES_TO_SYSTEM" || "$COMMAND_CP_CA_CERTIFICATES_TO_SYSTEM"
echo "#--- RootCA.crt copied to /usr/local/share/ca-certificates and Updated"
fi
/var/docker/docker-xnmp-vhosts/data/nginx/bin/createRootCA.shcreateNginxSslConfigFileExample.sh
#!/bin/bash
CERT_DIR=../certificates
ENABLED_DIR=../enabled
[ -f "$ENABLED_DIR/ssl.conf" ] && echo -e "Error Script: $ENABLED_DIR/ssl.conf already exist!" && exit 0
# RootCA
[[ ! -f "$CERT_DIR/RootCA.pem" || ! -f "$CERT_DIR/RootCA.key" ]] && ./createRootCA.sh && echo RootCA CREATED
# localhost
[[ ! -f "$CERT_DIR/localhost/localhost.crt" || ! -f "$CERT_DIR/localhost/localhost.key" ]] && ./createLocalhost.sh localhost && echo localhost.* CREATED
OUTPUT=`cat <<'EOF'
server {
server_name ~^ssl(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain.conf;
server_name ~^ssl(\.localhost)?$;
index index.html index.php;
root /vhosts/ssl/httpdocs;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
include /nginx/snippets/php-fpm-default.conf;
}
# SUBDOMAINS
server {
server_name ~^((?<subdomain>.*)\.)ssl(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain.conf;
server_name ~^((?<subdomain>.*)\.)ssl(\.localhost)?$;
index index.html index.php;
root /vhosts/ssl/subdomains/${subdomain}/httpdocs;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
include /nginx/snippets/php-fpm-default.conf;
}
EOF
`
# OUTPUT: EOF
echo -e "$OUTPUT" > $ENABLED_DIR/ssl.conf
echo DONE./var/docker/docker-xnmp-vhosts/data/nginx/bin/createNginxSslConfigFileExample.shrecreateNginxSslConfigFileExample.sh
#!/bin/bash
CERT_DIR=../certificates
ENABLED_DIR=../enabled
# [ -f "$ENABLED_DIR/ssl.conf" ] && echo -e "Error Script: $ENABLED_DIR/ssl.conf already exist!" && exit 0
# RootCA
./createRootCA.sh && echo RootCA CREATED
# localhost
./createLocalhost.sh localhost && echo "localhost.(crt|key|csr) CREATED"
OUTPUT=`cat <<'EOF'
server {
server_name ~^ssl(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain.conf;
server_name ~^ssl(\.localhost)?$;
index index.html index.php;
root /vhosts/ssl/httpdocs;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
include /nginx/snippets/php-fpm-default.conf;
}
# SUBDOMAINS
server {
server_name ~^((?<subdomain>.*)\.)ssl(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain.conf;
server_name ~^((?<subdomain>.*)\.)ssl(\.localhost)?$;
index index.html index.php;
root /vhosts/ssl/subdomains/${subdomain}/httpdocs;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
include /nginx/snippets/php-fpm-default.conf;
}
EOF
`
# OUTPUT: EOF
echo -e "$OUTPUT" > $ENABLED_DIR/ssl.conf
echo DONE./var/docker/docker-xnmp-vhosts/data/nginx/bin/recreateNginxSslConfigFileExample.shDownload bin.zip
The source inside you’ll find above information
Download script files and extract them to nginx/bin folder, shown above, then run:
./createNginxSslConfigFileExample.sh/var/docker/docker-xnmp-vhosts/data/nginx/binCertificates will be generated in this folder: /var/docker/docker-xnmp-vhosts/data/nginx/certificatesRestart Nginx to apply new configurations and visit to see:
- ssl.localhost
How to use the bin files
- When you want update or create SSL certificates for new (sub)domains you can append newline in dns.txt. For example:
ghost.localhost
*.ghost.localhost/var/docker/docker-xnmp-vhosts/data/nginx/bin/dns.txtThen run to update all your certificates:
./createLocalhost.sh/var/docker/docker-xnmp-vhosts/data/nginx/binRestart Nginx to apply new configurations and visit to see:
- ghost.localhost
- blog.ghost.localhost
- When you want to renew SSL RootCA Certificate, then run:
./createRootCA.sh
./createLocalhost.sh
/var/docker/docker-xnmp-vhosts/data/nginx/binInstall your generated RootCA.pem in your system, browser or device, follow guide from: Install Root Certificates
You might need to restart your computer to take effect
Restart Nginx to apply new configurations.
- When you want to start over again and regenerate the files, then run:
./recreateNginxSslConfigFileExample.sh/var/docker/docker-xnmp-vhosts/data/nginx/binRestart Nginx to apply new configurations.
Configure Letsencrypt for production
/etc/letsencrypt/
live/domain.ext/
fullchain.pem
privkey.pem
/var/docker/docker-xnmp-vhosts/
data/
nginx/
enabled/domain.ext.conf
snippets/
listen-ssl.conf
ssl-domain-ext.conf
vhosts/
domain.ext/
httpdocs/index.php
subdomains/
test/httpdocs/index.php
docker-compose.ymldomain.ext can be relplaced with your own domain ex: sylo.space
listen 443 ssl http2;
listen [::]:443 ssl http2;
# ssl_session_timeout 5m;
# ssl_protocols TLSv1.1 TLSv1.2;
# ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA';
# ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
/var/docker/docker-xnmp-vhosts/data/nginx/snippets/listen-ssl.confinclude /nginx/snippets/listen-ssl.conf;
ssl_certificate /etc/letsencrypt/live/domain.ext/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.ext/privkey.pem;
/var/docker/docker-xnmp-vhosts/data/nginx/snippets/ssl-domain-ext.conf# redirect 80 domain.ext to 443 ssl
server {
listen 80;
listen [::]:80;
server_name ~^domain\.ext(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain-ext.conf;
server_name ~^domain\.ext(\.localhost)?$;
root /vhosts/domain.ext/httpdocs;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
include /nginx/snippets/php-fpm-latest.conf;
}
# Subdomains
server {
listen 80;
server_name ~^((?<subdomain>.*)\.)domain\.ext(\.localhost)?$;
return 301 https://$host$request_uri;
}
server {
include /nginx/snippets/ssl-domain-ext.conf;
server_name ~^((?<subdomain>.*)\.)domain\.ext(\.localhost)?$;
root /vhosts/domain.ext/subdomains/${subdomain}/httpdocs;
autoindex on;
index index.html index.php;
location / {
try_files $uri $uri.html $uri/ /index.html /index.php$is_args$args;
include /nginx/snippets/snippet-location-root-cors.conf;
}
include /nginx/snippets/php-fpm-latest.conf;
}data/nginx/enabled/domain.ext.confversion: '2'
services:
data:
image: alpine:latest
command: /bin/sh
volumes:
- ./data/vhosts:/vhosts
- ./data/tmp:/tmp
- /etc/letsencrypt:/etc/letsencrypt:rodocker-compose.ymladd line on volumes: - /etc/letsencrypt:/etc/letsencrypt:roOptional: Dockerized certbot (Letsencrypt) runner for you live domain certificates
/var/docker/docker-xnmp-vhosts/
build/letsencrypt/
config/
domain.txt.example
email.txt.example
build/
certbot-alpine/
docker-entrypoint.sh
Dockerfile
docker-build.sh
docker-run.sh
data/
nginx/
enabled/
domain.ext.conf
domain2.ext.conf
vhosts/
domain.ext/
httpdocs/index.php
subdomains/
test/httpdocs/index.php
domain2.ext/
httpdocs/index.php
subdomains/
test/httpdocs/index.php
sub1/httpdocs/index.php
sub2/httpdocs/index.phpThis part only focus on theletsencrypt/. The other partsnginx/andvhosts/you need to configure yourself from what you have learned so far or whatever you need. I add this so you could have better reference.
Let’s say you want domain certificates with wildcards for example domain.ext and domain2.ext. It would be tedious work to run certbot every time for each domain you own. Of course you don’t have this problem if you only had just one domain to worry about, even though you can still use this method. So I created a script that checks list of domains and email from text files.
email.txt; where Letsencrypt send you an email when your certificates almost expire)
This script will run like an Interactive Shell, when it needs some of your input.
Before you run this script, have your DNS Records ready to edit or add TXT records.
Setup Dockerfile and bash scripts
FROM alpine:latest
RUN apk add --no-cache --update \
certbot \
bash
ENV HOME=/root
WORKDIR $HOME
ADD docker-entrypoint.sh $HOME
ENTRYPOINT ~/docker-entrypoint.sh
/var/docker/docker-xnmp-vhosts/build/letsencrypt/build/cerbot-alpine/Dockerfile#!/bin/bash
EMAIL=`cat email.txt`
SERVER=https://acme-v02.api.letsencrypt.org/directory
CERTBOTARGS="certonly --agree-tos -m $EMAIL --manual --manual-public-ip-logging-ok --preferred-challenges dns --server $SERVER"
DNSTXT=domains.txt
IFS=$'\r\n' GLOBIGNORE='*' command eval 'DOMAINS=($(<"$DNSTXT"))'; unset IFS
# cd /root/certbot
for DOMAIN in "${DOMAINS[@]}"
{
allDNS=($DOMAIN)
args=()
for dns in "${allDNS[@]}"; do args+=(-d "$dns"); done
echo "###: $DOMAIN"
certbot $CERTBOTARGS --cert-name ${allDNS[0]} "${args[@]}"
echo ================================
}
/var/docker/docker-xnmp-vhosts/build/letsencrypt/certbot-alpine/docker-entrypoint.sh#!/bin/bash
docker build -t harianto/certbot-alpine build/certbot-alpine/var/docker/docker-xnmp-vhosts/build/letsencrypt/docker-build.sh#!/bin/bash
docker run --rm \
-v $PWD/config/email.txt:/root/email.txt:ro \
-v $PWD/config/domains.txt:/root/domains.txt:ro \
-v $PWD/../../data:/etc/letsencrypt \
-it harianto/certbot-alpine/var/docker/docker-xnmp-vhosts/build/letsencrypt/docker-run.shConfig examples
you@domain.ext/var/docker/docker-xnmp-vhosts/build/letsencrypt/config/email.txt.exampledomain.ext *.domain.ext
domain2.ext *.domain2.ext *.sub1.domain2.ext *.sub2.domain2.extbuild/letsencrypt/config/domains.txt.exampleManual: Letsencrypt Certificate Maker
# Letsencrypt Certificate Maker
Little more automation with multiple domains and wildcards
## Build Once
Run
```bash
# Build a docker image: harianto/certbot-alpine
./docker-build.sh
```
## Config Files
In `config` directory you’ll find `.example` files or create:
`email.txt` and `domains.txt`
### email.txt
Create `email.txt`
```txt
you@domain.ext
```
> Put your email address where Letsencrypt can notify you when your ceritifcates almost expires
### domains.txt
Create `domains.txt`
```txt
domain.ext *.domain.ext
domain2.ext *.domain2.ext *.sub1.domain2.ext *.sub2.domain2.ext
```
> Each line need to be unique domain as `domain` DOT `ext` name, and follow subdomains that needs a wildcard `*`.
> For example: `*.domain.ext`
## Create certificates
Make sure you already build once with `docker-build.sh`.
Also you put your valid email.txt and domains.txt.
Run
```bash
# run image harianto/certbot-alpine
./docker-run.sh
```
> This will run in Interactive Shell mode while you need to follow and have time to set up your DNS tables
> All letsencrypt magic will be stored in `data` directory
## Notes
Make sure **docker-compose.yml** link correct folders in `nginx:`
```yml
service:
nginx:
volumes:
- ./data/letsencrypt:/etc/letsencrypt:ro
```
> Snippet: `docker-compose.yml`
/var/docker/docker-xnmp-vhosts/build/letsencrypt/README.mdIn short:
- run once:docker-build.sh
- config email and domains
- run:docker-run.sh
Optional Add-On: certbot-plugin-gandi
Since I’m using Gandi registrar, I manually edit the records with their web system manager. It was tedious when I need to renew certificates for 4 domains, then I made myself a bit more easy using their API with self-made node script. Actually I was planning to learn some python, and going to make a plugin for certbot. Good thing somebody already made it.
Here are modified script for using certbot plugin.
/var/docker/docker-xnmp-vhosts/
build/
letsencrypt/
config/
APIKEY
APIKEY.example
gandi.ini
gandi.ini.example
build/
certbot-alpine/
docker-entrypoint.sh
Dockerfile
docker-run.sh#!/bin/bash
EMAIL=`cat email.txt`
SERVER=https://acme-v02.api.letsencrypt.org/directory
CERTBOTARGS="certonly --agree-tos -m $EMAIL --manual-public-ip-logging-ok --preferred-challenges dns --server $SERVER -a certbot-plugin-gandi:dns --certbot-plugin-gandi:dns-credentials gandi.ini"
DNSTXT=domains.txt
IFS=$'\r\n' GLOBIGNORE='*' command eval 'DOMAINS=($(<"$DNSTXT"))'; unset IFS
# cd /root/certbot
for DOMAIN in "${DOMAINS[@]}"
{
allDNS=($DOMAIN)
args=()
for dns in "${allDNS[@]}"; do args+=(-d "$dns"); done
echo "###: $DOMAIN"
certbot $CERTBOTARGS --cert-name ${allDNS[0]} "${args[@]}"
echo ================================
}
/var/docker/docker-xnmp-vhosts/build/letsencrypt/build/certbot-alpine/docker-entrypoint.shgandiAPIkeyGANDIapiKeyXX/var/docker/docker-xnmp-vhosts/build/letsencrypt/config/APIKEY.examplecreate APIKEY file and get API key from Gandi# live dns v5 api key
certbot_plugin_gandi:dns_api_key=APIKEY
# optional organization id, remove it if not used
certbot_plugin_gandi:dns_sharing_id=SHARINGID/var/docker/docker-xnmp-vhosts/build/letsencrypt/config/gandi.ini.examplecreategandi.inifile and changeAPIKEYto$APIKEY(Yes, dollar sign) as an environmental variable.
FROM alpine:latest
RUN apk add --no-cache --update \
certbot \
python3 \
py3-pip \
bash \
&& pip3 install --upgrade pip \
&& pip install certbot-plugin-gandi
ENV HOME=/root
WORKDIR $HOME
ADD sh/renew-crt.sh $HOME
ENTRYPOINT ~/renew-crt.sh
/var/docker/docker-xnmp-vhosts/build/letsencrypt/build/certbot-alpine/Dockerfile#!/bin/bash
docker run --rm \
-e APIKEY=`cat config/APIKEY` \
-v $PWD/config/email.txt:/root/email.txt:ro \
-v $PWD/config/domains.txt:/root/domains.txt:ro \
-v $PWD/data:/etc/letsencrypt \
-it harianto/certbot-alpine/var/docker/docker-xnmp-vhosts/build/letsencrypt/docker-run.shNow I can just run ./docker-run.sh and automate the process. Done!
Share network
You can share network, by append this snippet below.
# create network: docker network create xnmp-network
networks:
default:
external:
name: xnmp-network/var/docker/docker-xnmp-vhosts/docker-compose.ymlThen create a network xnmp-network
# Run once
docker network create xnmp-networkYou can with other docker-compose.yml together in shared network.
Things I connect trough this shared network
Combined Docker Compose(s) through Docker Network