1

Install and configure a database and a webserver – Migrated to a new server 2 of 4

This is the second section in the 4 part series of migrating my server

  1. Install and configure the host machine
  2. Install and configure a database and webserver - we are here
  3. Install and configure a mailserver
  4. Install and configure vaultwarden
  5. Tie everything back to 1. for backups, misc, etc

Dockerized services

I will configure a database and a webserver(mainly wordpress container) to listen internally only and not exposed to the outside network. I will also configure a nginx-proxy and letsencrypt to provide the access from the world to the wordpress site and to generate the letsencrypt certificate.

Wanted to note that I like to create a directory where I mount to all of my containers so that I can easily transfer files between containers and the host. For this purpose I've created /opt/temp and I will add -v /opt/temp:/temp onto all the containers so that I can mount and share files back and forth between all the containers and the host.

The database and wordpress is specific for this section but the nginx-proxy and letsencrpyt can be used for other sections as well to provide a secure network access to other services.

Database service

Lets start with the database service. I am going to install mariadb. Why mariadb instead of mysql? not sure but since I been using mariadb for a while why not?

  • Create directories - I am using /opt/mariadb as my persistent storage for mariadb
mkdir -p /opt/mariadb
  • Create my docker-mariadb.service
[Unit]
Description=mariadb docker container
Requires=docker.service
After=docker.service

[Service]
Restart=always
RestartSec=90
ExecStartPre=-/usr/bin/docker kill mariadb
ExecStartPre=-/usr/bin/docker rm mariadb

ExecStart=/usr/bin/docker run \
        --name mariadb \
        --net internal \
        -e MARIADB_RANDOM_ROOT_PASSWORD=yes \
        -v /etc/localtime:/etc/localtime:ro \
        -v /opt/temp:/temp \
        -v /opt/mariadb:/var/lib/mysql \
        mariadb:latest

ExecStop=/usr/bin/docker stop mariadb

[Install]
WantedBy=multi-user.target
  • Enable and start mariadb
# systemctl daemon-reload
# systemctl enable docker-mariadb
# systemctl start docker-mariadb
  • Find the root password
$ docker logs mariadb | grep "GENERATED"
2021-12-19 00:35:26+00:00 [Note] [Entrypoint]: GENERATED ROOT PASSWORD: v(/-*lHy=Wn2bl?a.&hP&%#eg#cS*.[I
  • Secure mariadb - follow all the instructions from /usr/bin/mysql_secure_installation and remember to keep the new password safe
$ docker exec -it mariadb /bin/bash
root@b8e4dee1a09e:/# /usr/bin/mysql_secure_installation

NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!

In order to log into MariaDB to secure it, we'll need the current
password for the root user. If you've just installed MariaDB, and
haven't set the root password yet, you should just press enter here.

Enter current password for root (enter for none):
...

We now have a mariadb service running thats been secured and best part there are no network ports exposed at all

wordpress service

Since I will be hosting wordpress sites I will just run the wordpress containers instead of running a dedicated webserver.

  • Create directories
$ mkdir -p /opt/wp-site1
$ mkdir -p /opt/config
  • Create the service file /etc/systemd/system/docker-wp-site1.service
[Unit]
Description=wp-site1 docker container
Requires=docker-mariadb.service
After=docker.service

[Service]
Restart=always
RestartSec=90
ExecStartPre=-/usr/bin/docker kill wp-site1
ExecStartPre=-/usr/bin/docker rm wp-site1

ExecStart=/usr/bin/docker run \
        --name wp-site1 \
        --net internal \
        -e VIRTUAL_HOST=www.site1.com,site1.com \
        -e VIRTUAL_PORT=80 \
        -e LETSENCRYPT_EMAIL=your@email.address \
        -e LETSENCRYPT_HOST=www.site1.com,site1.com \
        --env-file /opt/config/env.site1 \
        -v /etc/localtime:/etc/localtime:ro \
        -v /opt/temp:/temp \
        -v /opt/wp-site1:/var/www/html \
        wordpress:latest

ExecStop=/usr/bin/docker stop wp-site1

[Install]
WantedBy=multi-user.target

VIRTUAL* will be used by ngnix-proxy to configure the proxy and LETSENCRYPT* will be used by the letsencrypt to request and generate the SSL certificate for nginx-proxy to use

  • Create /opt/config/env.wp-site1
WORDPRESS_DB_HOST=mariadb # name of your database container
WORDPRESS_DB_USER=siteuser # username for your database - dont use root
WORDPRESS_DB_PASSWORD=sitepass # password for your database
WORDPRESS_DB_NAME=wpsite1 # name of your database
  • Create your database and user
$ docker exec -it mariadb /bin/bash
root@52c97fb980b3:/# mysql -u root --password=xxxxxxxx
MariaDB [(none)] > CREATE DATABASE wpsite1;
MariaDB [(none)] > GRANT ALL PRIVILEGES ON wpsite1.* TO "siteuser"@"%" IDENTIFIED BY "sitepass";
MariaDB [(none)] > FLUSH PRIVILEGES;
MariaDB [(none)] > exit
  • Enable and start my wordpress site
# systemctl daemon-reload
# systemctl enable docker-wp-site1
# systemctl start docker-wp-site1

We now have a wordpress site running! Too bad you can only access it from the localhost on port 8080 fix this!

letsencrypt service

Letsencrypt container will use certbot to generate valid SSL certificates for your server. My DNS is setup with a wildcard *.example.com to my IP to make things easier but you can configure your own specific DNS.

  • Create directory
$ mkdir -p /opt/certbot
$ mkdir -p /opt/nginx-proxy/certs
$ mkdir -p /opt/nginx-proxy/vhost
$ mkdir -p /opt/nginx-proxy/html
  • Create the service file /etc/systemd/system/docker-letsencrypt.service
[Unit]
Description=letsencrypt docker container
Requires=docker.service
After=docker.service

[Service]
Restart=always
RestartSec=90
ExecStartPre=-/usr/bin/docker kill letsencrypt
ExecStartPre=-/usr/bin/docker rm letsencrypt

ExecStart=/usr/bin/docker run \
        --name letsencrypt \
        --net internal \
        -e NGINX_PROXY_CONTAINER=nginx-proxy \
        -v /etc/localtime:/etc/localtime:ro \
        -v /opt/temp:/temp \
        -v /opt/nginx-proxy/certs:/etc/nginx/certs:rw \
        -v /opt/nginx-proxy/vhost.d:/etc/nginx/vhost.d:rw \
        -v /opt/nginx-proxy/html:/usr/share/nginx/html:rw \
        -v /var/run/docker.sock:/var/run/docker.sock:ro \
        -v acme:/etc/acme.sh \
        -e "DEFAULT_EMAIL=your@email.address" \
        nginxproxy/acme-companion

ExecStop=/usr/bin/docker stop letsencrypt

[Install]
WantedBy=multi-user.target

NGINX_PROXY_CONTAINER needs to be the container name of your ngnix-proxy. This container will use the http-01 method to validate and have the certs signed by letsencrypt. It will watch the docker daemon to see if any containers have VIRTUAL* envvars to create new certs and it will auto-renew certs when needed.

  • Enable and start the service
# systemctl daemon-reload
# systemctl enable docker-letsencrypt
# systemctl start docker-letsencrypt

nginx-proxy service

nginx-proxy will provide the network connectivy from the internet to your wordpress instances.

  • Create directories - most were created already from the previous step
$ mkdir -p /opt/nginx-proxy/htpasswd
  • Create /opt/nginx-proxy/proxy.conf - this is good for most but you might need to customize
# Increase max upload size
client_max_body_size 100G;

# Increase timeouts
send_timeout 100m;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 30m;

# Increase header buffer size
client_header_buffer_size 64k;
large_client_header_buffers 4 64k;

# Don't disable iframes
proxy_hide_header X-Frame-Options;

# gzip
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_proxied any;
gzip_disable "MSIE [2-6]\.";
  • Create our service file /etc/systemd/system/docker-nginx-proxy.service
[Unit]
Description=nginx-proxy docker container
Requires=docker.service
After=docker.service

[Service]
Restart=always
RestartSec=90
ExecStartPre=-/usr/bin/docker kill nginx-proxy
ExecStartPre=-/usr/bin/docker rm nginx-proxy

ExecStart=/usr/bin/docker run \
        --name nginx-proxy \
        --net internal \
        -p 80:80 \
        -p 443:443 \
        -v /etc/localtime:/etc/localtime:ro \
        -v /opt/temp:/temp \
        -v /opt/nginx-proxy/certs:/etc/nginx/certs:ro \
        -v /opt/nginx-proxy/vhost.d:/etc/nginx/vhost.d:rw \
        -v /opt/nginx-proxy/htpasswd:/etc/nginx/htpasswd:ro \
        -v /var/run/docker.sock:/tmp/docker.sock:ro \
        -v /opt/nginx-proxy/proxy.conf:/etc/nginx/conf.d/proxy.conf:ro \
        -v /opt/nginx-proxy/html:/usr/share/nginx/html:rw \
        jwilder/nginx-proxy:latest

ExecStop=/usr/bin/docker stop nginx-proxy

[Install]
WantedBy=multi-user.target

You can see that we are binding port 80 and 443 to this container. This container will monitor your docker socket and auto create proxies based on the VIRTUAL* settings on your docker containers.

  • Enable and start the service
# systemctl daemon-reload
# systemctl enable docker-nginx-proxy
# systemctl start docker-nginx-proxy

Now you should be able to browse to your www.example.com and configure your wordpress site. To spin up additional sites follow the same steps and create new service and directories for your other wordpress instances.

Lets secure our wordpress site. If you can remember from the first part of this series we installed fail2ban and configured it. I will setup a custom fail2ban jail for wp-login

  • Create a jail /etc/fail2ban/jail.d/wp-login.local - Since our container logs will also show in syslog on the host for both nginx-proxy and wordpress containers we can just read from /var/log/messages
[wp-login]
enabled     = true
port        = http,https
filter      = wp-login
logpath     = /var/log/messages
banaction   = wp-login
  • Create a filter /etc/fail2ban/filter.d/wp-login.conf - this was adjusted to fit the new format
[Definition]
failregex = ^.* docker.*:  -.*POST.*wp-login.php.*$
  • Create the action /etc/fail2ban/action.d/wp-login.conf
[Definition]

actionstart = iptables -N f2b-wplogin
              iptables -A f2b-wplogin -j RETURN
              iptables -I FORWARD -p tcp -m multiport --dports 80 -j f2b-wplogin

actionstop = iptables -D FORWARD -p tcp -m multiport --dports 80 -j f2b-wplogin
             iptables -F f2b-wplogin
             iptables -X f2b-wplogin

actioncheck = iptables -n -L FORWARD | grep -q 'f2b-wplogin[ \t]'

actionban = iptables -I f2b-wplogin 1 -s  -j DROP

actionunban = iptables -D f2b-wplogin -s  -j DROP
  • restart and reload fail2ban
# systemctl restart fail2ban
# fail2ban-client reload
  • Check in on the jail
# fail2ban-client status wp-login
Status for the jail: wp-login
|- Filter
|  |- Currently failed: 1
|  |- Total failed:     1
|  `- File list:        /var/log/messages
`- Actions
   |- Currently banned: 0
   |- Total banned:     0
   `- Banned IP list:

I can try to fail login on my wp-login and it does show up in the logs

in /var/log/messages

Dec 19 21:22:47 hostname docker[1521479]: x.x.x.x - - [19/Dec/2021:21:22:47 -0600] "POST /wp-login.php HTTP/1.1" 503 19461 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36"

in /var/log/fail2ban.log

2021-12-19 21:22:48,358 fail2ban.filter         [1519807]: INFO    [wp-login] Found x.x.x.x - 2021-12-19 21:22:47

Lastly we can see that our containers are not exposed on the network at all except for the nginx-proxy on port 80 and 443

CONTAINER ID   IMAGE                                 COMMAND                  CREATED          STATUS                 PORTS                                                                                                                                                                                                                       NAMES
daf109afe7c7   wordpress:latest                      "docker-entrypoint.s…"   34 minutes ago   Up 34 minutes          80/tcp                                                                                                                                                                                                                      wp-site1
343ca6f43e14   jwilder/nginx-proxy:latest            "/app/docker-entrypo…"   6 hours ago      Up 6 hours             0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp                                                                                                                                                    nginx-proxy
3e108dc3ae0a   nginxproxy/acme-companion             "/bin/bash /app/entr…"   6 hours ago      Up 6 hours                                                                                                                                                                                                                                         letsencrypt
52c97fb980b3   mariadb:latest                        "docker-entrypoint.s…"   3 days ago       Up 3 days              3306/tcp                                                                                                                                                                                                                    mariadb

jlim0930

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.