Static website in Satorix

A tutorial for getting a static HTML website working in Satorix. For a description of the default environment variables managed by the Satorix Dashboard and using environment variables in your website check the application environment variables article.

Static website configuration

In your website code we will be working with some files to get things running on your Satorix Hosting Cluster. Make sure your public HTML code is placed into a sub-folder called public/.

Files placed in the application root directory

Add a .gitlab-ci.yml into the root directory of your website. This is a basic setup to allow us to automatically deploy to your Satorix Hosting Cluster:

# We are using the Satorix base image from https://hub.docker.com/r/satorix/base/
image: 'satorix/base:18'

# Global caching directives.
cache:
  key: "$CI_PROJECT_ID"
  paths:
    - 'tmp/satorix/cache' # To cache buildpack output between runs.


.satorix: &satorix
  script:
    - gem install satorix --no-document
    - satorix


deploy_with_flynn:
  environment:
    name: $CI_COMMIT_REF_NAME
    url: "http://$CI_PROJECT_NAME.$CI_COMMIT_REF_SLUG.$SATORIX_HOSTING_NAMESPACE"
  stage: deploy
  only:
    - staging
    - production
  <<: *satorix

Files placed in the public/ directory

A public/nginx.conf template to configure the Staticsite Nginx server. This setup has includes for web server level redirects, proxy settings, and authentication.

<%
  def include_custom_template(template, indent = 0, the_binding = binding)
    path = File.join(File.dirname(__FILE__), 'includes', template)
    erb = ERB.new(File.read(path), nil, "-", next_eoutvar)
    erb.filename = path
    result = erb.result(the_binding)

    # Set indenting
    result.gsub!(/^/, " " * indent)
    result.gsub!(/\A +/, '')

    result
  end

  def next_eoutvar
    @next_eoutvar_index ||= 0
    @next_eoutvar_index += 1
    "_erbout#{@next_eoutvar_index}"
  end

  def use_canonical?
    !canonical_domain.nil? &&
    !canonical_domain.empty? &&
    !canonical_domain_protocol.nil? &&
    !canonical_domain_protocol.empty?
  end

  def canonical_domain
    ENV['SATORIX_CANONICAL_URI_HOST']
  end

  def canonical_domain_protocol
    ENV['SATORIX_CANONICAL_URI_PROTOCOL']
  end

  def canonical_uri
    "#{ canonical_domain_protocol }://#{ canonical_domain }" if use_canonical?
  end
%>

worker_processes 1;
daemon off;
error_log <%= ENV["APP_ROOT"] %>/nginx/logs/error.log;
events { worker_connections 4096; }
http {
  charset utf-8;
  log_format cloudfoundry '$http_x_forwarded_for - $http_referer - [$time_local] "$request" $status $body_bytes_sent';
  access_log <%= ENV["APP_ROOT"] %>/nginx/logs/access.log cloudfoundry;
  default_type application/octet-stream;
  include mime.types;
  sendfile on;
  gzip on;
  gzip_disable "msie6";
  gzip_comp_level 6;
  gzip_min_length 1100;
  gzip_buffers 16 8k;
  gzip_proxied any;
  gunzip on;
  gzip_static always;
  gzip_types text/plain text/css text/json text/javascript
        application/javascript application/x-javascript application/json
        application/rss+xml application/vnd.ms-fontobject application/x-font-ttf
        application/xml font/opentype image/svg+xml text/xml;
  gzip_vary on;
  tcp_nopush on;
  keepalive_timeout 30;
  port_in_redirect off; # Ensure that redirects don't include the internal container PORT - <%= ENV["PORT"] %>
  server_tokens off;
  <% if use_canonical? %>
    # Redirect all requests to the canonical domain.
    server {
      server_name _;
      listen <%= ENV["PORT"] %> default_server;
      return 301 <%= canonical_uri %>$request_uri;
    }
    <% else %>
    # No canonical domain defined, passing all requests to the main server block.
    <% end %>

    # Main server block.
    server {
      <% if use_canonical? %>
      server_name <%= canonical_domain %>;
      <% else %>
      server_name _;
      <% end %>
      listen <%= ENV["PORT"] %>;
      root <%= ENV["APP_ROOT"] %>/public;

      <%= include_custom_template('page_level_redirects.erb', 6, binding) %>
      <%= include_custom_template('proxy_configuration.erb', 6, binding) %>
      <%= include_custom_template('authentication.erb', 6, binding) %>
    }
}

Files placed in the nginx/conf/includes/ directory

An include file nginx/conf/includes/page_level_redirects.erb allows you to create redirects to be handled by the Nginx server directly:

# Page-level Redirects

#   Define your own custom page-level redirects below.
#
#   Examples:
#     Standard single page redirects:
#       location = /old-page-1 { return 301 /new-page-1; }
#       location = /old-page-2 { return 301 /new-page-2; }

# End Page-level Redirects

We create the include nginx/conf/includes/proxy_configuration.erb to configure upstream proxies that you want to filter from the access logs. This allows you to see the actual requesting IP address of the client:

# Proxy Configuration
#
#  Used to configure settings related to Flynn's interaction with proxies.
#  Add your custom proxy configuration details below.

<% if ENV['SATORIX_PROXY_IPS'] %>
  # Provide additional proxy IPs, as described at http://nginx.org/en/docs/http/ngx_http_realip_module.html.
  #
  # This is particularity useful for services like CloudFlare, using the example at:
  # https://support.cloudflare.com/hc/en-us/articles/200170706-How-do-I-restore-original-visitor-IP-with-Nginx-
  #
  # If required, this variable should be populated with a space-separated list of proxy IPs. Example:
  # 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 104.16.0.0/12 108.162.192.0/18 2c0f:f248::/32

  real_ip_recursive on;

  <% ENV['SATORIX_PROXY_IPS'].to_s.split(' ').each do |real_ip| %>
  set_real_ip_from <%= real_ip %>;
  <% end %>
<% end %>

# Use the internal Flynn network set X-Forwarded-For header for access IPs.
set_real_ip_from <%= ENV['SATORIX_REAL_IP_FROM'] || '100.100.0.0/16' %>;
real_ip_header X-Forwarded-For;

# End Proxy Configuration

The include file nginx/conf/includes/authentication.erb is used to add HTTP Basic authentication into the Nginx configuration and allows you to set an environment variable SATORIX_AUTHENTICATION_HTPASSWDS the content of which will be used to generate the htpasswd file:

# Authentication

<%-
  # The password_files hash defines which password files will be written out.
  # The generated password files should be ignored from version control.
  # Each desired password file should be specified as a key, with the value being a source for the file contents.
  # The contents should include hashed username/password combinations, separated by whitespace.
  # These can be generated using the htpasswd application, or an online tool like http://www.htaccesstools.com/htpasswd-generator/
  # For more info, see: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/
  password_files = {
    'htpasswd' => ENV['SATORIX_AUTHENTICATION_HTPASSWDS']
  }

  def password_file_location(filename)
    includes_location = File.expand_path(File.dirname(__FILE__))
    File.join( includes_location, filename )
  end

  password_files.each do |filename, raw_contents|
    contents = raw_contents.to_s.split.join("\n")
    File.open(password_file_location(filename), 'w') {|f| f.write(contents) } unless contents.empty?
  end

  allowed_without_auth = ENV['SATORIX_AUTHENTICATION_ALLOWED_IPS'].to_s.split
  allowed_without_auth = ['all'] if allowed_without_auth.empty?
-%>

# Allow listed networks to access without auth, otherwise require password if defined
location / {
  satisfy any;
<% allowed_without_auth.each do |target| -%>
  allow <%= target %>;
<% end -%>
<% if File.file?(password_file_location('htpasswd')) -%>
  auth_basic "Please Log In";
  auth_basic_user_file <%= password_file_location('htpasswd') %>;
<% end -%>
  deny all;
}

# End Authentication

Dashboard settings for HTTP Basic authentication

Add the environment variable SATORIX_AUTHENTICATION_HTPASSWDS to the environment you want to restrict access to. This sets the usernames and passwords to use for Nginx HTTP Basic authentication. Needs to be generated in the format created by the Apache tool htpasswd -nb username password or using an online generator. The ENVVAR should contain newline separated lists of username and hashed password. A use case for this is restricting access to your staging environment to only authorized users. Example input:

username:$apr1$vAxBKb8N$m0en1zabtHktHeFyT3j9y
alsoname:$apr1$vAxBKb8N$m0en1zabtHktHeFyT3j9y

If you want to skip HTTP authentication for an application set the Satorix default variable SATORIX_AUTHENTICATION_ALLOWED_IPS to all and do not create the SATORIX_AUTHENTICATION_HTPASSWDS variable. This is typically what you would do on the production environment.