DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Software Design and Architecture
  3. Cloud Architecture
  4. Tengine, Docker, and PHP Application Best Practices

Tengine, Docker, and PHP Application Best Practices

These best practices are demonstrated through a simple application that separates the web service and the underlying codebase.

Alberto Roura user avatar by
Alberto Roura
·
Jun. 28, 19 · Tutorial
Like (1)
Save
Tweet
Share
15.66K Views

Join the DZone community and get the full member experience.

Join For Free

Image title


From a DevOps point of view, the importance of a well-architected solution, with proper separation of responsibilities, is fundamental for the long-term success of any application. This case we are presenting today is a very simplified example made to showcase the concept and to be easily understood, but it sets the base to scale it on your own as you gain confidence on this topic. We will make use of Elastic Compute Service (ECS), Server Load Balancer (SLB) and Virtual Private Cloud (VPC), all very common Alibaba Cloud services that you should be familiar with.

Containers and Running Services

When using Docker, like in our case today, one should never run more than one function per container. Running more than one defeats the whole purpose of using containers, as adding them doesn't cost much in terms of resources. In my experience, as DevOps lead engineer, I saw too many projects made by others with supervisord managing multiple functions in a single container. This is considered an anti-pattern as makes it very hard to track, debug and scale them horizontally. Please notice that I'm using the word function, not process. The official Docker documentation has moved away from saying one "process" to instead recommending one "concern" or "function" per container.

In today's small example we are showing how to separate the responsibilities using a web service and an app service, as shown below:

1


When using PHP applications we have to make a choice about the web server to use, Apache being the one that would fit most cases. Apache is great in terms of modules, as you can use mod_php so Apache itself executes the PHP code directly. This means that you would only need one container and still be able to track and debug the application properly. It also helps in reducing the complexity of the deployment in websites that don't experience a high concurrency of connections.

Our goal today is to build a highly-scalable project using Tengine allows us to separate the web server from the application codebase itself very simply.

What Is Tengine?

Tengine is, in a nutshell, Nginx with superpowers. This web server is everything you love about Nginx but with some neat extra features like dynamic module loading (loading modules without recompiling), unbuffered request proxy forwarding (saving precious disk I/Os), support for dynamic scripting language (Lua) for config files, and way more. You can have a look at the full list of features in the Tengine official site.

This web server has been an open source project since December 2011. Made by the Taobao team, it's been used in sites like Taobao and Tmall, and used internally in Alibaba Cloud as a key part of their CDN's Load Balancers.

As you can see, Tengine has been tested in some of the busiest websites in the world.

Preparing the Files

As we mentioned, we are planning to build a very simple but scalable project where we will separate the web server from the application codebase. This is achieved by running two containers, the web server listening to user requests and the PHP FPM interpreter as backend. Because this is a "best practices" type of article, we are setting up a Server Load Balancer to leave a playground afterwards to scale the whole thing. At the end, we will deploy everything using Terraform, a tool used to create, change and improve infrastructure in a predictable way. 

docker-compose.yml

Given the maturity of the Docker Compose project, we are going to deploy this application by writing a docker-compose.yml file as shown below:

version: '3.7'

networks:
  webappnet:
    driver: bridge

services:
  app:
    image: php:7.3-fpm
    container_name: fpm
    networks:
    - webappnet
    restart: on-failure
    volumes:
    - ./index.php:/var/www/html/index.php
  web:
    image: roura/php:tengine-fpm
    container_name: tengine
    networks:
    - webappnet
    restart: on-failure
    ports:
    - 80:80
    volumes:
    - ./index.php:/var/www/html/index.php

As you can see, there are two services running in the network, app and web. The web service will pass all requests to the app one on port 9000, which is the one exposed by FPM by default. In the future, you can scale those two services independently and horizontally as needed.

index.php

Did I mention the app was going to be simple? You see, it has only one file, but enough to test that a PHP script is being rendered in the backend. Create the index.php next to the docker-compose.yml with the following content:

<?php phpinfo();

user-data.sh

This file will serve as the main template to bootstrap all services in the ECS instance. It basically installs all dependencies and creates the minimal files to start the containers.

#!/usr/bin/env bash

mkdir /var/docker

cat <<- 'EOF' > /var/docker/docker-compose.yml
${docker_compose}
EOF

cat <<- 'EOF' >/var/docker/index.php
${index_php}
EOF

apt-get update && apt-get install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get update && apt-get install -y docker-ce docker-compose
curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` -o /usr/bin/docker-compose

cd /var/docker && docker-compose up -d

main.tf

This is the file that will orchestrate it all. It creates a new VPC, a Server Load Balancer, and ECS without Internet IP and a Security Group locking it all down to the port 80 only. The file, named main.tf needs to be next to the others we just created and should look like the following:

provider "alicloud" {}

variable "app_name" {
  default = "webapp"
}

data "template_file" "docker_compose" {
  template = "${file("docker-compose.yml")}"
}

data "template_file" "index_php" {
  template = "${file("index.php")}"
}

data "template_file" "user_data" {
  template = "${file("user-data.sh")}"
  vars {
    docker_compose = "${data.template_file.docker_compose.rendered}"
    index_php = "${data.template_file.index_php.rendered}"
  }
}

data "alicloud_images" "default" {
  name_regex = "^ubuntu_16.*_64"
}

data "alicloud_instance_types" "default" {
  instance_type_family = "ecs.n4"
  cpu_core_count = 1
  memory_size = 2
}

resource "alicloud_vpc" "main" {
  cidr_block = "172.16.0.0/12"
}

data "alicloud_zones" "default" {
  available_disk_category = "cloud_efficiency"
  available_instance_type = "${data.alicloud_instance_types.default.instance_types.0.id}"
}

resource "alicloud_vswitch" "main" {
  availability_zone = "${data.alicloud_zones.default.zones.0.id}"
  cidr_block = "172.16.0.0/16"
  vpc_id = "${alicloud_vpc.main.id}"
}

resource "alicloud_security_group" "group" {
  name = "${var.app_name}-sg"
  vpc_id = "${alicloud_vpc.main.id}"
}

resource "alicloud_security_group_rule" "allow_http" {
  type = "ingress"
  ip_protocol = "tcp"
  nic_type = "intranet"
  policy = "accept"
  port_range = "80/80"
  priority = 1
  security_group_id = "${alicloud_security_group.group.id}"
  cidr_ip = "0.0.0.0/0"
}

resource "alicloud_security_group_rule" "egress" {
  type = "egress"
  ip_protocol = "tcp"
  nic_type = "intranet"
  policy = "accept"
  port_range = "80/80"
  priority = 1
  security_group_id = "${alicloud_security_group.group.id}"
  cidr_ip = "0.0.0.0/0"
}

resource "alicloud_slb" "main" {
  name = "${var.app_name}-slb"
  vswitch_id = "${alicloud_vswitch.main.id}"
  internet = true
}

resource "alicloud_slb_listener" "https" {
  load_balancer_id = "${alicloud_slb.main.id}"
  backend_port = 80
  frontend_port = 80
  health_check_connect_port = 80
  bandwidth = -1
  protocol = "http"
  sticky_session = "on"
  sticky_session_type = "insert"
  cookie = "webappslbcookie"
  cookie_timeout = 86400
}

resource "alicloud_slb_attachment" "main" {
  load_balancer_id = "${alicloud_slb.main.id}"
  instance_ids = [
    "${alicloud_instance.webapp.id}"
  ]
}

resource "alicloud_instance" "webapp" {
  instance_name = "${var.app_name}"
  image_id = "${data.alicloud_images.default.images.0.image_id}"
  instance_type = "${data.alicloud_instance_types.default.instance_types.0.id}"
  vswitch_id = "${alicloud_vswitch.main.id}"
  security_groups = [
    "${alicloud_security_group.group.id}"
  ]
  password = "Test1234!"
  user_data = "${data.template_file.user_data.rendered}"
}

output "slb_ip" {
  value = "${alicloud_slb.main.address}"
}

Putting Everything Together

Since we have all the files written, we are ready to go. Run terraform init && terraform apply and wait until all your infrastructure resources are created.

When everything finishes, you will see the output printing something like slb_ip = 47.xx.xx.164. That is the public IP of the Load Balancer. Copy it and paste it into your web browser. Ta-da! A screen like the following screenshot should show up:

2

Congratulations, you are a better cloud architect now!

PHP application Docker (software) Web Service Web server Cloud computing Virtual private cloud

Published at DZone with permission of Alberto Roura, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Getting a Private SSL Certificate Free of Cost
  • Microservices 101: Transactional Outbox and Inbox
  • Building a Real-Time App With Spring Boot, Cassandra, Pulsar, React, and Hilla
  • gRPC on the Client Side

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: