Optimal deployment environment for productivity boost

Your deployment environment is a tool that should be sharpened to allow maximum productivity. I have seen many developers where their deployment environment is less than optimal, hurting their productivity.

Ranging from, developing directly on production sites. To develop on a shared server. And finally using a local development environment, which I think is the most optimal way to do it.

Developing directly on production is “fast”, but remember the quote

Slow and steady wins the race

If you develop directly in production, the business will be all over you when you break things, and you will break things! So take the time to get a setup that allows you to go fast in the future.

A local development environment has many advantages and with Docker, it is easy to set up. I will show how I handle the setup for my development. Including tip for how you can take advantage of a local development environment.

The first part motivates why setting up a local development environment is a good idea. Feel free to skip directly to how we can handle it with Docker.

Production development environment

It should not require much explaining why it is a bad idea to develop directly in production systems. But I have seen it happen in a few cases for different reasons that might resonate with you.

Different developers

If the customer only requires small changes from time to time in a software product, they do not need continuous employment of the same developer(s). Often it will be a different developer that is contracted from task to task.

Setting up a development environment from scratch by a new developer can be a daunting and time-consuming task. Often the customer will not pay for the time needed to set up a development environment, because it might cost an order of a magnitude more than developing directly on the production system, even if the risk is higher.

Because of time or cost constraint, developing directly on production systems happen more than we want it to. It always carries a high risk to develop directly on a production system.

Small development needs

The production environment is always running because it needs to. If the development needs in a product are small the development environment could get into a state where it does not resemble production anymore, and make developing in that environment impossible. Forcing a developer to make the changes directly to production.

It is just a small fix!

A developer might be tempted to do a small fix in production because it is easier than trying to get the development environment running. A high-risk operation!

Developing on production happens for many different reasons, but it is always a high-risk endeavor.

Development server environment

A development server environment is better than developing directly on production, but it does have severe limitations depending on how the software product is built. We want the setup to resemble the production environment as much as possible. It is often a tradeoff because the cost for an exact copy of production is often too expensive.

Single developer

When a single developer works on a product, a development server can be a good way to handle the development task. A server is independent of the developer’s local machine making it is easier to have a stable environment.

It also gives the deployment process two steps, development and production, the risk of breaking production is much smaller. The business can even test new features before they are released to production. A much better setup than developing directly, on production.

Multiple developers

When using an interpreted programming language like PHP or Python, each file is read at runtime, so it is possible for multiple developers to use the same server at the same time. As long as they do not change the same files they will not override each other’s changes. Better but far from an optimal situation.

The problem arises because in many projects there are a few files that contain code that almost all other components depend on, and when they need to change for multiple developers at the same time they will override each other’s changes. It could be files with dependency injection code or ORM setups or similar, where there are often changes from multiple developers.

I have seen many projects where changes are pushed to the shared environment using FTP. It contains no guards for when files are overridden, causing confusion when the change you just made does not show up in the software.

As the number of developers increases, this causes more and more problems and breaks down eventually.

Be wary of changes to the environment

If you do not take care to track changes to your environment the production and development environment will start to diverge. Change happens all the time.

  • A new version of the database software gets upgraded on production but not to development
  • A config setting is changed on the web server but forgotten in development
  • An operating system upgrade changed a configuration that causes problems on one setup but not the other.
  • And so on.

Each small change cause potential problems if it is not synchronized between the environments.

Local development environment

The final step is to have an environment for each developer that they have exclusive access to. Their own computer.

Running the development locally has many advantages. There is no competition with other developers, no risk of overwriting each other’s changes. Production is unaffected if changes break the product. Debugging is easier to set up. Changes made to files show up instantly.

The problem with configuration management across the environment is still there. Docker can help in this case.

Setting up the environment with Docker

Docker gives us the power to replicate as close to production as possible, with minimal effort.

An added challenge for me is that my production setup runs on Linux and my development machine is a Windows machine. But Docker can help bridge that gap. When changing from a single production setup to production, test, and local development environments, the configuration management challenge increase a lot. Docker can help us manage it while making development a breeze.

So how does development with Docker looks like?

Initially, I only had a production setup, shame on me 🙂 Production run Docker swarm, and are defined as shown here:

version: "3.2"

services:
  ############## loadbalancer ################
  loadbalancer:
    image: 637345297332.dkr.ecr.eu-west-1.amazonaws.com/patch-loadbalancer:latest
    build: loadbalancer
    ports:
      - target: 8080
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
   
    deploy:
      placement:
        constraints:
          - node.role == manager
    volumes:
          - /etc/letsencrypt:/etc/letsencrypt
          - /var/lib/letsencrypt:/var/lib/letsencrypt

  fileserver:
    image: 637345297332.dkr.ecr.eu-west-1.amazonaws.com/patch-fileserver:latest
    build: fileserver
    deploy:
      restart_policy:
        condition: none
      
    volumes:
      - /data/storage/patch_wp-core/_data:/var/wordpress/
      - /data/storage/patch_lund-fitness-data/_data/:/var/lund-fitness.dk

  ############ DB server ###################
  db:
    image: mariadb
    volumes:
      - db-data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=xxx
    deploy:
      placement:
        constraints:
          - node.role == manager

      resources:
        limits:
          memory: 300M

  backup-db:
    image: 637345297332.dkr.ecr.eu-west-1.amazonaws.com/patch-backup-sql:latest
    environment:
      - MYSQL_USER=root
      - MYSQL_PASSWORD=xxx
      - MYSQL_HOST=db
      - AWS_BUCKET_LOCATION=EU
      - AWS_ACCESS_KEY=xxx
      - AWS_SECRET_KEY=xxx
      - AWS_BUCKET=s3://sqlbackup.patch.dk/
    
    deploy:
      placement:
        constraints:
          - node.role != manager
      resources:
        limits:
          memory: 100M
   
  backup-files:
    image: thomaslomas/s3cmd-cron
    environment:
      - AWS_ACCESS_KEY=xxx
      - AWS_SECRET_KEY=xxx
      - AWS_BUCKET=s3://sqlbackup.patch.dk/wp-content/
      - AWS_BUCKET_LOCATION=EU
      - BACKUP_DIR=/data/
   
    volumes:
      - /data/storage/patch_datadriven-investment-data/_data:/backup/datadriven-investment:ro
      - /data/storage/patch_broderi-info-data/_data:/backup/broderi-info:ro
      - /data/storage/patch_nordic-safe-data/_data:/backup/nordic-safe:ro
      - /data/storage/patch_lund-fitness-data/_data:/backup/lund-fitness:ro
      - /data/storage/patch_super3booster-data/_data:/backup/super3booster:ro

    deploy:
      placement:
        constraints:
          - node.role != manager
      resources:
        limits:
          memory: 100M

  #################### WEBSITES ############
  http:
    image: 637345297332.dkr.ecr.eu-west-1.amazonaws.com/patch-httpd:latest
    build: httpd
   
    deploy:
      mode: global

      resources:
        limits:
          memory: 50M
     
    volumes:
      - /data/storage/patch_wp-core/_data:/var/www/nordic-safe.com/:ro
      - /data/storage/patch_wp-core/_data:/var/www/datadriven-investment.com/:ro
      - /data/storage/patch_wp-core/_data:/var/www/broderi-info.dk/:ro
      - /data/storage/patch_wp-core/_data:/var/www/lund-fitness.dk/:ro
      - /data/storage/patch_wp-core/_data:/var/www/super3booster.dk/:ro

      - /data/storage/patch_nordic-safe-data/_data:/var/www/nordic-safe.com/wp-content:ro
      - /data/storage/patch_datadriven-investment-data/_data:/var/www/datadriven-investment.com/wp-content:ro
      - /data/storage/patch_broderi-info-data/_data:/var/www/broderi-info.dk/wp-content:ro
      - /data/storage/patch_lund-fitness-data/_data:/var/www/lund-fitness.dk/wp-content:ro
      - /data/storage/patch_super3booster-data/_data:/var/www/super3booster.dk/wp-content:ro

  php:
    image: 637345297332.dkr.ecr.eu-west-1.amazonaws.com/patch-php-fpm:latest
    build: php-fpm

    environment:
      - PHP_VALIDATE_TIMESTAMPS=1
      - PHP_DISPLAY_ERRORS=0
      - PHP_ERROR_REPORTING=E_ALL

    deploy:
      mode: global
  
      resources:
        limits:
          memory: 150M

    volumes:
      - /data/storage/patch_wp-core/_data:/var/www/nordic-safe.com/:ro
      - /data/storage/patch_wp-core/_data:/var/www/datadriven-investment.com/:ro
      - /data/storage/patch_wp-core/_data:/var/www/broderi-info.dk/:ro
      - /data/storage/patch_wp-core/_data:/var/www/lund-fitness.dk/:ro
      - /data/storage/patch_wp-core/_data:/var/www/super3booster.dk/:ro

      - /data/storage/patch_nordic-safe-data/_data:/var/www/nordic-safe.com/wp-content
      - /data/storage/patch_datadriven-investment-data/_data:/var/www/datadriven-investment.com/wp-content # write allowed
      - /data/storage/patch_broderi-info-data/_data:/var/www/broderi-info.dk/wp-content # write allowed
      - /data/storage/patch_lund-fitness-data/_data:/var/www/lund-fitness.dk/wp-content # write allowed
      - /data/storage/patch_super3booster-data/_data:/var/www/super3booster.dk/wp-content # write allowed

  redis:
    image: redis:4.0.8-alpine

    deploy:
      placement:
        constraints:
          - node.role != manager

      resources:
        limits:
          memory: 100M
      
############## Data persisted on host #######
volumes:
  db-data: # database files
  influx:
    driver: local
  grafana:
    driver: local

There is no need to run swarm mode in development, there is no need to have load balancing and multiple web servers, and backup is not needed either. To boot the development environment I created a scaled down version of the compose file to deploy using docker-compose:

version: "3"

services:
  fileserver:
    build: fileserver
      
    environment:
      - SKIP_COPY_THEME=1
    volumes:
      - wordpress-data:/var/wordpress
      - './development/lund-fitness.dk/ignore:/var/lund-fitness.dk'

  db:
    image: mariadb
    volumes:
      - './development/sqldump/:/var/sqldump'

    environment:
      - MYSQL_ROOT_PASSWORD=xxx

  php:
    build: php-fpm
  
    environment:
      - PHP_VALIDATE_TIMESTAMPS=1
      - PHP_DISPLAY_ERRORS=1
      - PHP_ERROR_REPORTING=E_STRICT 
      - PHP_XDEBUG_REMOTE_HOST=10.0.75.1 # host machine's IP so xdebug can connect to the IDE
      - PHP_XDEBUG_ZEND=/usr/local/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so

    volumes:
      - wordpress-data:/var/www/lund-fitness.dk/
      - './development/lund-fitness.dk/wp-content:/var/www/lund-fitness.dk/wp-content'
      - './fileserver/lund-fitness:/var/www/lund-fitness.dk/wp-content/themes/lund-fitness'

  http:
    build: httpd
    ports:
      - "80:80"

    volumes:
      - wordpress-data:/var/www/lund-fitness.dk/
      - './development/lund-fitness.dk/wp-content:/var/www/lund-fitness.dk/wp-content'
      - './fileserver/lund-fitness:/var/www/lund-fitness.dk/wp-content/themes/lund-fitness'

  redis:
    image: redis:4.0.8-alpine

volumes:
  wordpress-data:

It uses the exact same Docker images as production but with a few changed configuration variables. First all unneeded services are removed. The load balancer is skipped and port 80 on the http service is mapped to allow access.

Volumes from the local development environment are mapped. This allows us to change the files in our IDE and see the changes instantly.

The php-fpm service has a few extra settings to allow easier development. In production .php files are read and cached forever. Obviously, we do not want this behavior in production so it is disabled with the setting PHP_VALIDATE_TIMESTAMPS. Error reporting level is also increased and xdebug is enabled.

It allows a development environment that closely resembles the production environment but still allows settings to facilitate debugging.

Managing databases

To be able to run the website locally we need a database dump to seed the local database. I used a backup from the production site. Over time the production database and local database will diverge. To import data into the database I use the command.

docker exec patch_db_1 sh -c “mysql -uroot -pxxx < /var/sqldump/lund_fitness.sql”
It executes the mysql command inside the database container which has the sqldump folder mounted. So in this way, we can edit the database file directly in our IDE and import it into the database container.
You could also manipulate the database directly using a database management tool, but that will cause problems if the development machine crashes or we need to onboard a new developer. Using a database dump file that we can commit to the source repository makes this easier.

Closing comments

I hit a few gotchas when setting up the environment.

First, a strange error that the fileserver service script reported strange errors. This happened because of windows line endings, as explained here. It was difficult to diagnose but easy to fix.

It is possible to use environment variables directly inside php.ini files as described here. It makes different settings for development and production easy.


Also published on Medium.