Apache server on Docker with HTTPS

Łukasz Pawłowski
codeburst
Published in
8 min readApr 27, 2020

--

If you work in web development, you probably use Docker as a virtualization tool. There is also a high probability that the same images your team use locally are used in stage or production. Probably your stage/production uses HTTPS communication. In that case, you need Docker with HTTPS. Let me show you how we do it for the LAMP stack. I’ll explain to you why we do it and how we do it.

Why Docker with SSL

Like many other companies, we use Docker for a dev environment. We use it also for tests and preview for clients. It gives us the possibility to unify server configuration across all environments. To make it easier to run full LAMP stack, we use docker-compose to organize multiple containers for one application.

SSL is currently standard on the Internet. Not using it is a kind of bad practice. Even if you do not care about security, there are some web APIs in web browsers, which are not available if you do not use https communication. Locally developers do not need SSL — in most cases, the localhost is treated the same way as https hosts. But when you deploy your application for tests or as a preview for a client, and it is available via the Internet, you should handle communication with HTTPS.

As you see, enabling HTTPS communication on our docker containers is very important.

General idea

First, I need to explain how it should work.

We have a private server with Ubuntu, which serves as host for containers. Also, DNS directs to that server.

Each web application runs on separate sets of containers. We use docker-compose to configure services for each app.

For this article, let’s assume we use apache2 as an HTTP server for both host and container. On the main server (host) you would probably prefer Nginx, but we’ll stick with apache for presentation purposes.

Each HTTPS request will hit our host server. The host server will use Reverse Proxy to pass communication to the selected container. To make it simpler for us, we’ll also map 80 and 443 ports from the container on selected ports from the host. Our main server would pass HTTP and HTTPS communication to the localhost on selected ports.

The General assumption for the presented example

Let’s assume that:

  • Our domain is test-https-docker.com
  • We’ll keep our application under /srv/www/web_apps/test_https on host. In subfolder code we keep our codebase. In subfolder apache_log we keep apache2 logs from the container. Subdirectory ssl will contain certificate files.
  • We’ll use letsencrypt’s SSL certificates.
  • For presentation purposes, we use default settings in most cases, which should be adjusted in real applications.
  • Logs from host for apache will be kept in folder /var/www/log/test-https-docker.com
  • We’ll map 14080 host’s port to 80 container’s port, and 14443 host’s port to 443 container’s port.

Docker initialization

First, we need to create a docker container.

We create /srv/www/web_apps/test_https folder and two subfolders: code, apache_log. We move to code subfolder and create a simple PHP file index.php:

<?php 
echo "This is example page with HTTPS on docker"

Now we need docker-compose.yml (in the same directory as index.php):

version: '3.1'
services:
webserver:
image: php:7.3-apache
restart: always
volumes:
- ./:/var/www/html
- /srv/www/web_apps/test_https/apache_log:/var/log/apache2

We use php:7.3-apache image. We attach host’s directory /srv/www/web_apps/test_https/code as /var/www/html on container. Also we attach /srv/www/web_apps/test_https/apache_log as /var/www/logs/apache2, to get access to dockers apache logs from host.

Host HTTP configuration

If we run `docker-compose up`, our container will work and contain the page, but we are not able to open it outside the host. We need to create a virtual host in apache configuration on the host. It should be a proxy, which will direct HTTP traffic to our container. For that, we can use container IP or port mapping. For presentation purposes, we ignore any load balancer and more complex security and use simple mapping. We’ll map port 14080 from host to port 80 on the container, by adding two lines for webserver definition in docker-compose.yml:

ports:
- "14080:80"

Next, we add a new virtual-host definition on the host. We start by creating /var/www/log/test-https-docker.com for logs. Then, we add test-https-docker.com.conf in /etc/apache2/sites-available (in host):

<VirtualHost *:80>
ServerName test-https-docker.com
ServerAdmin webmaster@test-https-docker.com
CustomLog /var/www/log/test-https-docker.com/custom.log combined
ErrorLog /var/www/log/test-https-docker.com/errors.log
ProxyPreserveHost On
ProxyPass "/" "http://127.0.0.1:14080/"
ProxyPassReverse "/" "http://127.0.0.1:14080/"
</VirtualHost>

We need to enable the site and restart apache:

a2ensite test-https-docker.com.conf
service apache2 restart

DNS configuration is out of the scope of this article, let’s assume that DNS is configured correctly and our domain direct to our host server. In that case, after rebuilding container, we should be able to open our test-https-docker.com

Add support for multiple environments

In most cases, our application will run on multiple environments: dev, test, preview, production, etc. For each environment, we probably want to use different ports and volumes. To do that, we’ll use .env file.

In our code folder we add new file .env:

HOST_HTTP_PORT=14080
APACHE_LOG_VOLUME=/srv/www/web_apps/test_https/apache_log

We also need to adjust our docker-compose.yml:

version: '3.1'
services:
webserver:
image: php:7.3-apache
restart: always
volumes:
- ./:/var/www/html
- ${APACHE_LOG_VOLUME}:/var/log/apache2
ports:
- "${HOST_HTTP_PORT}:80"

If we use git we should include .env in .gitignore. A good practice is to create .env.example, which will present what and how can be configured.

HTTPS configuration for Host

Now we should move to SSL configuration. We’ll use letsencrypt certificate created with cerbot. We need to install certbot on host:

sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot python-certbot-apache

Now we can use certbot to create a certificate. We can also automatically create a virtual host configuration for it. We start with command:

sudo certbot --apache

We’ll be asked to select which domain we want to use. Select the correct one. After that, we’ll be asked about the configuration of a virtual host — select option without redirection of HTTP to HTTPS. Normally you probably would redirect whole HTTP traffic to HTTPS, but for presentation purposes, we do not do that.

When scripts finish, you should be able to find /etc/apache2/sites-available/test-https-docker.com-le-ssl.conf with SSL configuration. Now, when we would open https://test-https-docker.com we would see ower page. Also, http://test-https-docker.com should still work.

HTTPS configuration on container

We have a working page on the container, and it is available through https. So, are we done? Not really. Currently, we have an https connection with the host server, but communication between host and container is not secured. Also, our domain configuration on the container is not correct. Let’s fix it.

First, we’ll need to copy our certificate into the container. We must use the same certificates on the host and container. The location of the certificate can be found in the virtual host configuration. In our case, we need files: /etc/letsencrypt/live/test-https-docker.com/fullchain.pem and /etc/letsencrypt/live/test-https-docker.com/privkey.pem. We create directory/srv/www/web_apps/test_https/ssl on our host and copy there both .pem files created with certbot:

cp /etc/letsencrypt/live/test-https-docker.com/fullchain.pem /srv/www/web_apps/test_https/ssl/fullchain.pemcp /etc/letsencrypt/live/test-https-docker.com/privkey.pem /srv/www/web_apps/test_https/ssl/privkey.pem

Next, we want to make sure that files are available in the container. Let’s assume we’ll keep them under /var/imported/ssl directory. We add volume for webserver in our docker-compose.yml:

- ${SSL_VOLUME}:/var/imported/ssl

And add it to our .env file

SSL_VOLUME=/srv/www/web_apps/test_https/ssl

You can ask why we copied files instead of using the original created by certbot. We should be able to attach original files as volumes. If we would do that we can have problems with file ownership and permission for files. It is better to duplicate files and manage permissions separately.

Next, we’ll adjust virtual host configurations on container. To simplify we’ll use default apache files. We create new file /srv/www/web_apps/test_https/code/docker/000-default.conf

<VirtualHost *:80>
ServerName test-https-docker.com
ServerAdmin webmaster@test-https-docker.com
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName test-https-docker.com
ServerAdmin webmaster@test-https-docker.com
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLCertificateFile /var/imported/ssl/fullchain.pem
SSLCertificateKeyFile /var/imported/ssl/privkey.pem
SSLEngine on
</VirtualHost>
</IfModule>

Now we need to make sure, that this file is used by container’s apache. We add the next volume for webserver in our docker-compose.yml:

- ./docker/000-default.conf:/etc/apache2/sites-available/000-default.conf

When apache is configured, we should also map https ports, as we did for HTTP. First we add new variable in .env:

HOST_HTTPS_PORT=14443

Then we add next port mapping for webserver in docker-compose.yml:

- "${HOST_HTTPS_PORT}:443"

After rebuilding the container, we should have correctly configured apache on it with https and certificates.

Host configuration adjustments

The last step would be the adjustment of the apache configuration on the host server. Let’s change our /etc/apache2/sites-available/test-https-docker.com-le-ssl.conf. We need to change our proxypass definitions:

ProxyPass        "/" "https://127.0.0.1:14443/"
ProxyPassReverse "/" "https://127.0.0.1:14443/"

Also, we need to adjust the proxy configuration to use SSL correctly:

SSLProxyEngine On
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLProxyCheckPeerExpire off
ProxyPreserveHost On

The first line activates SSL for proxy. Next four lines disable certificate verification in communication between host and container. The last line defines that the host will pass the Host: line from the incoming request to the container instead of the hostname specified in the ProxyPass line.

Now we can restart apache on the host, and we should have a working configuration of SSL on docker.

Last thoughts

When you work on real systems, you’ll need to adjust your apache configuration on the container and host to fit your requirements. It is always a good idea to redirect all HTTP traffic to HTTPS

Don’t forget to verify files permissions and ownerships, especially for attached volumes. If it is required, you can create your own Dockerfile and adjust entry point to run chmod and chown commands on each container startup.

Many systems require additional configuration to set the correct base URL and secure URL for the application. Make sure that you adjust them too in your system’s configuration files and databases.

It is worth considering to keep all configuration files on /docker directory. If you use CI and CD you can verify the configuration and deploys it on the server automatically. This way php.ini, apache config, etc. can also be versioning. Attach such files as volumes. This way, you would not need to build the whole image with each change. Also, remember to give the possibility to adjust any volumes path with .env to keep any secrets outside your git repositories.

Summary

Let’s summarized what we did:

  • We installed certbot and created SSL certificate for our domain on the host server. We used the same certificates in the container.
  • We mapped the container’s ports 80 and 443 to selected ports on the host. We configured apache on a host as proxy and redirect HTTP and HTTPS to mapped ports.
  • We attached certificates and apache configuration files as volumes on the container.
  • We used .env file to define paths to volumes and ports we mapped. That gives us flexibility in a configuration for multiple environments.

This is our final https configuration on host:

<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName test-https-docker.com
ServerAdmin webmaster@test-https-docker.com
CustomLog /var/www/log/test-https-docker.com/custom.log combined
ErrorLog /var/www/log/test-https-docker.com/errors.log
ProxyPass "/" "https://127.0.0.1:14443/"
ProxyPassReverse "/" "https://127.0.0.1:14443/"
SSLProxyEngine On
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLProxyCheckPeerExpire off
ProxyPreserveHost On
SSLCertificateFile /etc/letsencrypt/live/test-https-docker.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/test-https-docker.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>

This is our HTTP configuration on host:

VirtualHost *:80>
ServerName test-https-docker.com
ServerAdmin webmaster@test-https-docker.com
CustomLog /var/www/log/test-https-docker.com/custom.log combined
ErrorLog /var/www/log/test-https-docker.com/errors.log
ProxyPreserveHost On
ProxyPass "/" "http://127.0.0.1:14080/"
ProxyPassReverse "/" "http://127.0.0.1:14080/"
</VirtualHost>

Here is our apache configuration on container, kept in /srv/www/web_apps/test_https/code/docker/000-default.conf:

<VirtualHost *:80>
ServerName test-https-docker.com
ServerAdmin webmaster@test-https-docker.com
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName test-https-docker.com
ServerAdmin webmaster@test-https-docker.com
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLCertificateFile /var/imported/ssl/fullchain.pem
SSLCertificateKeyFile /var/imported/ssl/privkey.pem
SSLEngine on
</VirtualHost>
</IfModule>

This is content of .env file from /srv/www/web_apps/test_https/code

HOST_HTTP_PORT=14080
APACHE_LOG_VOLUME=/srv/www/web_apps/test_https/apache_log
HOST_HTTPS_PORT=14443
SSL_VOLUME=/srv/www/web_apps/test_https/ssl
APACHE_CONF=./docker/000-default.conf

This is our docker-compose.yml kept in /srv/www/web_apps/test_https/code

version: '3.1'
services:
webserver:
image: php:7.3-apache
restart: always
volumes:
- ./:/var/www/html
- ${APACHE_LOG_VOLUME}:/var/log/apache2
- ${APACHE_CONF}:/etc/apache2/sites-available/000-default.conf
- ${SSL_VOLUME}:/var/imported/ssl
ports:
- "${HOST_HTTP_PORT}:80"
- "${HOST_HTTPS_PORT}:443"

--

--

Web developer, tech advisor, manager, husband & father. Tech Manager at Boozt