Docker-based hosting on your own VPS preview

Docker-based hosting on your own VPS

| Read time: 8 minute(s)

I was tempted to host all my clients' site by myself after reading Andrew Welch's post on hosting almost 2 years ago. As I am a developer in love with Docker, I thought using it for production would be fun. Well... it is definitely fun.

First start with what are our main objectives:

  • All domains served from the VPS should automatically receive an SSL certificate, without human intervention.
  • The newly deployed sites and domains should be available right after they are deployed and the domains are routed to the VPS.
  • The projects should have environments completely separated each other.
  • Version control the project configurations.
  • The projects should have separately configurable stacks. This means one project could run on specific versions of PHP, MySQL, Apache, Nginx or whatever, another project could run a completely different version of the same (or on a completely different stack).
  • Keep the VPS configuration on the bare minimum, in order to have a fast escape plan to another VPS if something goes bad.

The requirements above can be achieved by an infrastructure based on Docker:

  • Every project contains its own production configuration. This means they have to "ship" a complete service set (web server, database, PHP) in their e.g. docker-compose.yml file.
  • The project will have its own stand-alone web server, with a specific set of domains defined in its section of docker-compose.yml. The reverse proxy will route the requests to this web server if the domain matches one of the domains specified in that set.
  • Only the reverse proxy is available from the Internet.
  • The reverse proxy listens on ports 80 and 443, but every request is automatically redirected to HTTPS.
  • The certificates needed for HTTPS are automatically generated by its companion container via Let's Encrypt.

Below is an illustration of this infrastructure:

Docker Based Hosting With Vps

The "paths" on the image are there just for the sake of presentation, they are completely hypothetical and configurable.

NB: The article assumes you have a preconfigured VPS with Docker and Docker Compose installed. If you don't have one yet, spin up one before continuing

Reverse proxy

The files needed for our reverse proxy are available in this GitLab repository. Clone the repo and enter the newly created folder.

bash
git clone https://gitlab.com/webmenedzser/hosting-with-docker.git && cd hosting-with-docker

Here you can find a docker-compose.yml file with the content below:

yaml
networks:
  nginx_reverse_proxy:
    external: false

version: '2.3'
services:

  nginx_reverse_proxy:
    build:
      context: .nginx_reverse_proxy
      dockerfile: Dockerfile
    container_name: nginx_reverse_proxy
    ports:
      - 80:80
      - 443:443
    restart: always
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - .nginx_reverse_proxy/certs:/etc/nginx/certs:ro
      - .nginx_reverse_proxy/vhost.d:/etc/nginx/vhost.d
      - /usr/share/nginx/html
    networks:
      - nginx_reverse_proxy
    labels:
      - com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_reverse_proxy

  letsencrypt_nginx_proxy_companion:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: letsencrypt_nginx_proxy_companion
    restart: always
    volumes_from:
     - nginx_reverse_proxy
    volumes:
     - /var/run/docker.sock:/var/run/docker.sock:ro
     - .nginx_reverse_proxy/certs:/etc/nginx/certs:rw

It is not that complicated: we mount the host folders where they should be mounted (^.^) and set up some defaults. For details head to the documentation of the reverse proxy, the companion image, and the Nginx documentation.

Important: this configuration works only with browsers supporting SNI (so a combination of Windows XP + Internet Explorer 8 is out of the question), but in return, it will score an A+ on the test of Qualys SSL Labs.

Project configuration

To have our reverse proxy route the traffic properly, each project should indicate which requests belong to it. This is done via the VIRTUAL_HOST and LETSENCRYPT_HOST environment variables of the web server of the project: the first contains the set of domains which should be routed to the web server, and the second one indicates for which domains should the companion container acquire an SSL certificate. These two sets overlap completely in almost every case, so we could use a .env file next to our docker-compose.yml in our project defining these variables. Moreover, we have to connect our project web server (blue discs in our illustration above) to the network of the reverse proxy (in this case the name of the network is nginx_reverse_proxy).

.env:

VIRTUAL_HOST=www.wbmngr.agency,wbmngr.agency,cdn.wbmngr.
[email protected]

docker-compose.yml:

yaml
volumes:
  database_volume: {}

networks:
  nginx_reverse_proxy:
    external: true
  default:
    external: false

version: '3.6'
services:

  web:
    image: webmenedzser/craftcms-nginx:latest
    container_name: ${COMPOSE_PROJECT_NAME}_web
    working_dir: /var/www/
    restart: always
    volumes:
      - ./:/var/www/
    depends_on:
      - database
      - php
    expose:
      - 80
    networks:
      - default
      - nginx_reverse_proxy
    environment:
      - VIRTUAL_HOST=${VIRTUAL_HOST}
      - LETSENCRYPT_HOST=${VIRTUAL_HOST}
      - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
    logging:
      driver: json-file
      options:
        max-size: '1m'
        max-file: '3'

  php:
    image: webmenedzser/craftcms-php:latest
    container_name: ${COMPOSE_PROJECT_NAME}_php
    working_dir: /var/www
    volumes:
      - ./:/var/www/
    logging:
      driver: json-file
      options:
        max-size: '1m'
        max-file: '3'

  database:
    image: mariadb:latest
    container_name: ${COMPOSE_PROJECT_NAME}_database
    restart: always
    environment:
     - MYSQL_USER=${DB_USER}
     - MYSQL_PASSWORD=${DB_PASSWORD}
     - MYSQL_DATABASE=${DB_DATABASE}
     - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
    volumes:
     - database_volume:/var/lib/mysql
    expose:
     - 3306
    networks:
     - default
    logging:
      driver: json-file
      options:
        max-size: '1m'
        max-file: '3'

As you can see, we use far more variables than those two I wrote about: this way we can reuse our docker-compose.yml file in all of our projects: Docker (as well as Craft - you see where we are heading? :) ) can use the .env file, so we only need to change the content of the .env file in our projects, the rest is handled by the variables and the docker-compose.yml.

More noteworthy bits:

  • we don't map the containers ports to any "real" ports on the host: they are only available through the Docker network,
  • we set log rotation (max 3 files) and small file size (max. 1 MB each),
  • the containers are restarted no matter what happens (this is important... :P)

Let's spin up our project (docker-compose up -d)! The letsencrypt_nginx_proxy_companion container will be triggered by the event emitted by the containers starting up, so it will initiate the certificate generation process (if it can't find a suitable certificate already present) - you can check it with docker logs letsencrypt_nginx_proxy_companion. I couldn't figure out yet why I ended up in a redirect loop if the cloud icon in Cloudflare is orange (=the additional CF features, including the free SSL certificates, are active), so if you know what causes this, please, ping me and I will update this article. Update: Matt Stein advised me to set Crypto -> SSL from Flexible to Full. It seems that solved the issue - thanks Matt! If your non-www or www subdomain suffers from a redirect loop, set up a redirect rule from that subdomain to the other one (e.g.: non-www stuck in redirect loop: set up a redirect rule from non-www to www). 

If you (and I) did everything right, you now have a server with an A+ HTTPS connection, your projects can run on it completely separated and you can host your clients' project on it until it can handle the traffic. Yay!

You can find more details about the images used in the project example here: 

PS: on DigitalOcean you still get credits worth $100 for new registrations - I don't know if this is a permanent or a temporary promotion, but if you want to give the world of VPS a try, this a good opportunity. The credits are valid for 60 days.

If you have any question or you have found an error/bug, please get in touch!