Setup local domain and SSL for PHP-apache container

I currently have a simple docker setup in which I’m using PHP:7.3.30-apache and I’m able to view my website at http://localhost:8000.
What is the best way to create a custom domain like “http://local-docker” instead of localhost?
In addition to this I would like to enable SSL certificates for this local domain.

I’ve found a lot of solutions that involve adding a reverse proxy but I’m not sure if this is exactly what I’m looking for.

docker-compose.yml:

version: '3.8'

services:

  php-apache-environment:
    container_name: php-apache
    build:
      dockerfile: ./.docker/php/Dockerfile
    depends_on:
      - db
    volumes:
      - ./public:/var/www/html
    ports:
      - 8000:80

  db:
    container_name: db
    image: mariadb:10.1.48
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: testje
    volumes:
      - db_data:/var/lib/mysql
      - db_conf:/etc/mysql
    ports:
      - "9906:3306"

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    environment:
      PMA_HOST: mariadb
    ports:
        - '8080:80'
    environment:
        PMA_HOST: db
    depends_on:
        - db

# Volumes
volumes:
  db_data:
  db_conf:

Dockerfile:

FROM php:7.3.30-apache
RUN a2enmod rewrite
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli && docker-php-ext-install pdo_mysql
RUN apt-get update && apt-get upgrade -y

You are asking for more than one thing:

1 - add a line to your computer’s host file (/etc/hosts for Linux; C:\Windows\System32\drivers\etc\hosts for Windows) pointing the desired name to 127.0.0.1, i.e. adding this hostname to the line starting with 127.0.0.1

127.0.0.1  localhost local-docker

2 - create a certificate + key matching this hostname
To create a self-signed certificate using OpenSSL only for local-docker with an expirationdate 1 year in the future you can use this command

openssl req -x509 -new -out mycert.crt -keyout mycert.key -days 365 -newkey rsa:4096 -sha256 -nodes

and answer the questions to your best knowledge. Important part is the Common Name which has to be the one you have added to your hosts-file above
Example:

Country Name (2 letter code) [AU]:DE
State or Province Name (full name) [Some-State]:Baden-Wuerttemberg
Locality Name (eg, city) []:Pforzheim
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Privat
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:local-docker
Email Address []:

3 - add certificate and enable SSL
add your key mycert.key to image’s directory /etc/ssl/private/
add your certificate mycert.crt to image’s directory /etc/ssl/certs/
You can do so either by ADDing these files to your image during creation within the Dockerfile or mount it later using the docker-compose.yml. I will use the latter option.

Add this commands to your Dockerfile to enable SSL:

a2enmod ssl && a2enmod socache_shmcb

modify /etc/apache2/sites-available/default-ssl.conf to point to your certificate + key so that the lines starting with SSLCertificate... read

SSLCertificateFile      /etc/ssl/certs/mycert.crt
SSLCertificateKeyFile /etc/ssl/private/mycert.key

You can do so by adding these commands to your docker-compose.yml

RUN sed -i '/SSLCertificateFile.*snakeoil\.pem/c\SSLCertificateFile \/etc\/ssl\/certs\/mycert.crt' /etc/apache2/sites-available/default-ssl.conf
RUN sed -i '/SSLCertificateKeyFile.*snakeoil\.key/cSSLCertificateKeyFile /etc/ssl/private/mycert.key\' /etc/apache2/sites-available/default-ssl.conf

Enable the SSL-enabled site with a2ensite default-ssl within your Dockerfile.

4 - Forward a port to container’s SSL-port
add a port of your choice to be forwarded to container’s port 443 to the php-apache-environment-section within your docker-compose.yml so that it reads (using port 8443 for SSL in this example)

  ....
  ports:
    - 8000:80
    - 8443:443
  ....

At the end my Dockerfile looks like this

FROM php:7.4-apache
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli && docker-php-ext-install pdo_mysql
RUN a2enmod rewrite && a2enmod ssl && a2enmod socache_shmcb
RUN sed -i '/SSLCertificateFile.*snakeoil\.pem/c\SSLCertificateFile \/etc\/ssl\/certs\/mycert.crt' /etc/apache2/sites-available/default-ssl.conf && sed -i '/SSLCertificateKeyFile.*snakeoil\.key/cSSLCertificateKeyFile /etc/ssl/private/mycert.key\' /etc/apache2/sites-available/default-ssl.conf
RUN a2ensite default-ssl
RUN apt-get update && apt-get upgrade -y

and my docker-compose.yml is this (skipped the MariaDB-part because not needed for this sort-of-tutorial)

version: '3.8'

services:
    php-apache-environment:
        container_name: php-apache
        build: ./
        volumes:
          - ./mycert.crt:/etc/ssl/certs/mycert.crt
          - ./mycert.key:/etc/ssl/private/mycert.key
          - /var/www/html:/var/www/html
        ports:
          - 8000:80
          - 8443:443

Now you should be able to access your Docker-container using https://local-docker:8443 :slight_smile:

Hope this helps or at least point you into the right direction?

4 Likes

Thanks for taking your time to respond!
I’m going to try this out later today.

Does the following line enable both localhost and local-docker or does this replace localhost with local-docker?

127.0.0.1  localhost local-docker

I’m asking because I would like to have multiple websites running simultaneously in different docker containers.

Would the following line work for this purpose?

127.0.0.1  localhost local-docker local-anothersite local-anothersite2

I’m also a little confused that there is no configuration needed to point the server to the new domain other than the SSL certificate.
What if I wanted to use a custom local domain without SSL?

Hello,

editing the hosts-file is similar to setting some DNS-entries but only visible to your local computer.
So: Yes, you can add as many entries to one IP-address as you want to have.
And: This has nothing to do with SSL or not-SSL - you still can decide to access a port with http and another one with https.

Right now you can access every container with every hostname as your computer only decides which container to access based on the destination-TCP-port.
The SSL-certificate is only used like a passport presented by the webserver/container to your browser to check/ensure you are still talking to the correct host.

For the future you can do more advanced stuff - using a loadbalancer (like traefik) listening on one port :443 and decides where (which webserver/container) to send the request to based on the requested hostname.
Also you can create your own CA (certificate authority) and/or using certificates with SAN (subject alternate name = certficate is valid for multiple hostnames/ip-addresses/domains)

Did it work? I got error “this site can’t provide a secure connection”

Hello,
what is the exact errormessage?
I guess that you browser is complaining about the self-signed certificate used for encryption. You can either simply continue (with some mouse-clicks) or setup your own CA and add this to the list of trusted CAs within your browser.
Or the browser could complain about a certificate without a SAN - in the meantime using SAN is mandatory (it depends on the browser you are using).

You might want to check this discussion regarding CA/SAN:

Even though the topic is about docker itself being affected, it still applies to this case here as well.

Hi,
I followed your steps, but still my browser displays that I’m in HTTP and not HTTPS.
Certificate is created, port should be 443, I added certificate filess, I checked /etc/apache2/sites-available/default-ssl.conf file and everything seems to be ok… but I’m still not able to connect using SSL (port 443).
So what did I do wrong ?

@alainroger:
Some questions for troubleshooting:

  • What Dockerfile was used to create the image?
  • Have you checked with docker-compose logs for (error-)messages within the container?
  • Check within your docker-compose.yml that port 443 is available to the outside-world.
  • Check within your container that there is a symlink within the directory /etc/apache2/sites-enabled/ to /etc/apache2/sites-available/default-ssl.conf (should be created with the command a2ensite default-ssl)
  • Is there an error-message within your browser?
  • What Dockerfile was used to create the image?
    FROM php:5.6.40-apache

If think the main issue is with the self-signed certificate. Each browser displays information differently. While Firefox display that I added an exception, FF display https:// before the “local-docker” web root

Edge, Chrome and Brave browsers: display: NOT SECURE and strikethrough “https://” protocol

docker-compose logs
Nothing special in logs :frowning:

docker-compose.yml
Here is my docker compose file section regarding php:

php:
    restart: always
    container_name: php56
    build:
      context: ./web-server/php
      dockerfile: dockerfile_php56
    volumes:
      - ./public_html:/var/www/html
      - ./web-server/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
      - ./web-server/cert/mycert.crt:/etc/ssl/certs/mycert.crt
      - ./web-server/cert/mycert.key:/etc/ssl/private/mycert.key
    ports:
      - 80:80
      - 443:443
    environment:
      - PHP_DISPLAY_ERRORS=1
      - PHP_MEMORY_LIMIT=2048M
      - PHP_MAX_EXECUTION_TIME=300
      - PHP_POST_MAX_SIZE=500M
      - PHP_UPLOAD_MAX_FILESIZE=256M
      # the next 2 lines avoid to have permissions issue as in the container owner of /var/www/html is www-data and on computer is your username
      - APACHE_RUN_USER="#1000"
      - APACHE_RUN_GROUP="#1000"

So port 443 and 80 are correctly forwarded

  • No error with browser

If it is about the self-signed-certificate you can create your own CA and use this to sign the webserver’s certificate.

For testing a self-signed-certificate might be fine, but to avoid corrersponding error-messages you need a certificate signed by a CA.

Here are some steps to create your own CA and a webserver-certificate including SAN (subject alternate name) which is mandatory since a few years.

create CA
first create a Certificate Authority. For this you have to crate a private key

openssl genrsa -aes256 -out ca-key.pem 4096

The key is named ca-key.pem and has a length of 4096 bits. The key is passwort-protected (because of the -aes256-option) and has to be kept secure as a bad guy can create/sign arbitrary certificates which are trusted by the clients.

Now that a secret key for the CA is available we need the root-certificate which has to be imported by the clients/browsers to trust the certificates issued/signed by this CA.
The root-certificate ca-root.pem is created with the following command - you may need the password for the key created in the step above:

openssl req -x509 -new -nodes -extensions v3_ca -key ca-key.pem -days 1024 -out ca-root.pem -sha512

In this case the CA will be valid 1024 days. During creation you will be asked for some attributes for the CA - an example:

Enter pass phrase for ca-key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:DE
State or Province Name (full name) [Some-State]:Baden-Wuerttemberg
Locality Name (eg, city) []:Pforzheim
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Example-Company
Organizational Unit Name (eg, section) []:IT
Common Name (e.g. server FQDN or YOUR name) []:ca.example.com
Email Address []:admin@example.com

Now you can import the ca-root.pem-file into your browser’s/computer’s truststore.

create a certificate for the webserver
As the CA is completed we can create our first certificate.
A private key is the base. Similar to the CA a private key is created:

openssl genrsa -out webserver-key.pem 4096

Adding a password is not practicable in most cases as webserver have to ask for the password at every startup.
Now we will create a CSR - some attributes will be asked. The field Common Name has to be filled with the hostname the clients will connect to (either an ip-address 192.168.2.2 or DNS-name www.example.com). You can leave the challenge-password empty:

openssl req -new -key webserver-key.pem -out webserver.csr -sha512

If I remember correctly from earlier tests the FQDN of the CA’s certificate and the FQDN of the webserver’s certificate have to be different.

Create an extfile for the webserver’s certificate which contains at least one line for alt_names (you can add multiple lines DNS.2 = ..., DNS.3 = ..., … to create a certificate valid for multiple hostnames) as newer browers don’t trust the subject-fields’s cn:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = www.example.com

The webserver.csr can now be processed by the CA. This will create the public key for the private.key. Both (the webserver-key.pem and the webserver-pub.pem) will be needed on the webserver for encryption.
The webserver-pub.pem will be created using the following command and will be valid for 365 days:

openssl x509 -req -in webserver.csr -CA ca-root.pem -CAkey ca-key.pem -CAcreateserial -out webserver-pub.pem -days 365 -sha512 --extfile webserver.ext

The -CAcreateserial is automatically skipped if a serial-file is present which will be used in this case.
The webserver-key.pem and webserver-pub.pem can now be used within your webserver’s configuration for encryption.
Your Browsers should trust your certificate and only give some minor hint that it is signed by a CA added manually and not trusted by default.

verify that your certificate is signed correctly

openssl verify -verbose -CAfile root-ca.pem webserver-pub.pem
1 Like

amazing, it works in one try , thanks Man