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. - This article expects you already have Docker installed on your machine and have root
privileges and know some terminal commands. This article is base on a PopOS/Debian computer. - Tip: Post-installation steps for Linux to run docker
without sudo
Article Updates
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
file: /var/docker/docker-xnmp-vhosts/docker-compose.yml
Then run command from /var/docker/docker-xnmp-vhosts/
directory.
# Start docker containers from compose file
docker-compose up
Run commands from: /var/docker/docker-xnmp-vhosts/
You can also run this as background process: docker-compose up -d
From 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>
file: /var/docker/docker-xnmp-vhosts/data/vhosts/_default_/httpdocs/index.html
Create 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;
}
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/enabled/_default_.conf
Restart 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
Run commands from: /var/docker/docker-xnmp-vhosts/
This command will use already running nginx container and execute a command nginx -t
for 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.html
File Structure After 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>
file: /var/docker/docker-xnmp-vhosts/data/vhosts/example.com/httpdocs/index.html
Virtual #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>
file: /var/docker/docker-xnmp-vhosts/data/vhosts/example.com/subdomains/antique/httpdocs/index.html
Virtual #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>
file: /var/docker/docker-xnmp-vhosts/data/vhosts/example.com/subdomains/beauty/httpdocs/index.html
Nginx 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;
}
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/enabled/example.com.conf
Nginx 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.php
File Structure PHP 5 & 7 & 8 Create simple phpinfo files in the vhosts directory <?php phpinfo()
file: index.php
file: /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;
#}
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/snippet-php-fastcgi.conf
location ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 5-6-23-fpm:9000;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/php-5.6.23-fpm.conf
location ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 7-0-8-fpm:9000;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/php-7.0.8-fpm.conf
location ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 8-0-0-fpm:9000;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/php8.0.0-fpm.conf
Apply 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;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/enabled/php5.conf
server {
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;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/enabled/php7.conf
server {
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;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/enabled/php8.conf
Restart 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.php
File Structure Add legacy project in Virtual Host directory <?php phpinfo()
file: /var/docker/docker-xnmp-vhosts/data/vhosts/php7-ext/httpdocs/index.php
Create Nginx snippet and domain config location ~ \.php(/|$) {
include /nginx/snippets/snippet-php-fastcgi.conf;
fastcgi_pass 7-0-8-fpm-ext:9000;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/php-7.0.8-fpm-ext.conf
server {
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;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/enabled/php7-ext.conf
Create 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
file: /var/docker/docker-xnmp-vhosts/build/7-0-8-fpm-ext/Dockerfile
php: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
file: /var/docker/docker-xnmp-vhosts/build/8-0-0-fpm-ext/Dockerfile
Already 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
zlib
Already included from the default image: 8.0.0-fpm-alpine PHP 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:
- data
New service: 7-0-8-fpm-ext , snippet: docker-compose.yml
Snippet 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:
- data
New service: 8-0- 0-fpm-ext , snippet: docker-compose.yml
Snippet Link Nginx with the new service
services:
nginx:
links:
- 7-0-8-fpm-ext
- 8-0-0-fpm-ext
Linked service 7-0-8-fpm-ext and 8-0-0-fpm-ext , snippet: docker-compose.yml
Snippet 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
file: /var/docker/docker-xnmp-vhosts/docker-compose.yml
This 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
Run commands from: /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.conf
File Structure For 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
Run commands from: /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.conf
File Structure Nginx 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;
}
# }
file: /var/docker/docker-xnmp-vhosts/nginx/snippets/snippet-server-location-upstream.conf
server {
server_name ~^dbadmin(\.localhost)?$;
resolver 127.0.0.11 valid=30s;
set $upstream http://dbadmin:80;
include /nginx/snippets/snippet-server-location-upstream.conf;
}
file: /var/docker/docker-xnmp-vhosts/nginx/enabled/dbadmin.conf
Restart Nginx to apply new configurations and visit to see: - dbadmin.localhost
host: db, username: root, password: root To change database root password edit docker-compose.yml
and look for MYSQL_ROOT_PASSWORD /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.php
File Structure Post-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
done
There seem to be more versions of libnssckbi.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-kit
Found 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()
file: /var/docker/docker-xnmp-vhosts/data/vhosts/ssl/httpdocs/index.php
file: /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;
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/snippet-ssl.conf
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
include /nginx/snippets/snippet-ssl.conf;
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/ssl-defaultserver.conf
listen 443 ssl;
listen [::]:443 ssl;
include /nginx/snippets/snippet-ssl.conf;
file: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/ssl-domain.conf
Optional: 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;
}
file: /var/docker/docker-xnmp-vhosts/data/nginx/enabled/ssl.conf
Scripts 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
file: /var/docker/docker-xnmp-vhosts/data/nginx/bin/dns.txt
createDomainDirectory.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
file: /var/docker/docker-xnmp-vhosts/data/nginx/bin/createDomainDirectory.sh
createLocalhost.sh #!/bin/bash
./createDomainDirectory.sh localhost
file: /var/docker/docker-xnmp-vhosts/data/nginx/bin/createLocalhost.sh
createRootCA.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
file: /var/docker/docker-xnmp-vhosts/data/nginx/bin/createRootCA.sh
createNginxSslConfigFileExample.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.
file: /var/docker/docker-xnmp-vhosts/data/nginx/bin/createNginxSslConfigFileExample.sh
recreateNginxSslConfigFileExample.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.
file: /var/docker/docker-xnmp-vhosts/data/nginx/bin/recreateNginxSslConfigFileExample.sh
Download 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
Run script from: /var/docker/docker-xnmp-vhosts/data/nginx/bin
Certificates will be generated in this folder: /var/docker/docker-xnmp-vhosts/data/nginx/certificates
Restart 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
snippet: /var/docker/docker-xnmp-vhosts/data/nginx/bin/dns.txt
Then run to update all your certificates:
./createLocalhost.sh
Run script from: /var/docker/docker-xnmp-vhosts/data/nginx/bin
Restart 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
Run script from: /var/docker/docker-xnmp-vhosts/data/nginx/bin
Install 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
Run script from: /var/docker/docker-xnmp-vhosts/data/nginx/bin
Restart Nginx to apply new configurations.
/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.yml
File Structure domain.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;
File: /var/docker/docker-xnmp-vhosts/data/nginx/snippets/listen-ssl.conf
include /nginx/snippets/listen-ssl.conf;
ssl_certificate /etc/letsencrypt/live/domain.ext/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.ext/privkey.pem;
File: /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;
}
File: data/nginx/enabled/domain.ext.conf
version: '2'
services:
data:
image: alpine:latest
command: /bin/sh
volumes:
- ./data/vhosts:/vhosts
- ./data/tmp:/tmp
- /etc/letsencrypt:/etc/letsencrypt:ro
Snippet: docker-compose.yml
add line on volumes: - /etc/letsencrypt:/etc/letsencrypt:ro
Optional: 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.php
File Structure This part only focus on the letsencrypt/
. The other parts nginx/
and vhosts/
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
File: /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 ================================
}
File: /var/docker/docker-xnmp-vhosts/build/letsencrypt/certbot-alpine/docker-entrypoint.sh
#!/bin/bash
docker build -t harianto/certbot-alpine build/certbot-alpine
File: /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
File: /var/docker/docker-xnmp-vhosts/build/letsencrypt/docker-run.sh
Config examples you@domain.ext
File: /var/docker/docker-xnmp-vhosts/build/letsencrypt/config/email.txt.example
domain.ext *.domain.ext
domain2.ext *.domain2.ext *.sub1.domain2.ext *.sub2.domain2.ext
File: build/letsencrypt/config/domains.txt.example
Manual: 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`
File: /var/docker/docker-xnmp-vhosts/build/letsencrypt/README.md
In 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
File Structure
#!/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 ================================
}
File: /var/docker/docker-xnmp-vhosts/build/letsencrypt/build/certbot-alpine/docker-entrypoint.sh
gandiAPIkeyGANDIapiKeyXX
File Example: /var/docker/docker-xnmp-vhosts/build/letsencrypt/config/APIKEY.example
create 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
File Example: /var/docker/docker-xnmp-vhosts/build/letsencrypt/config/gandi.ini.example
create gandi.ini
file and change APIKEY
to $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
File: /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
File: /var/docker/docker-xnmp-vhosts/build/letsencrypt/docker-run.sh
Now 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
snippet: /var/docker/docker-xnmp-vhosts/docker-compose.yml
Then create a network xnmp-network
# Run once
docker network create xnmp-network
You can with other docker-compose.yml together in shared network. Things I connect trough this shared network Combined Docker Compose(s) through Docker Network
Sources GitHub: Cross-Platform (X), Nginx (N), MariaDB (M), PHP (P)