LogoLogo
  • Welcome to Release
  • Getting started
    • Quickstart
    • Create an account
    • Prepare to use Release
    • Create an application
      • Create custom application
      • Create from template
      • Servers vs runnables
    • Create an environment
  • Guides and examples
    • Domains and DNS
      • Manage domains
      • DNS and nameservers
        • Configure GoDaddy
        • Configure Cloudflare
        • Configure Namecheap
        • Other DNS hosts
      • Routing traffic
    • Example applications
      • Full stack voting app
      • Flask and RDS counter app
      • Static site with Gatsby
      • Golang with Postgres and Nginx
      • WordPress with MySQL
      • Spring and PostgreSQL
      • Terraform and Flask
      • OpenTelemetry demo
      • Load balancer with hostname
      • Static JavaScript service
      • SSH bastion access to services
      • ngrok and OAuth for private tunnels
      • Using OAuth Proxy
      • Hybrid Docker and static site
      • App Imports: Connecting two applications
      • Example library
    • Running instances
      • Cron jobs
      • Jobs
      • Using Helm charts
      • Using terminal
      • Viewing logs
      • Troubleshooting
        • ImagePullBackoff error
        • CrashLoopBackoff error
        • Exit codes
        • OOM: out of memory
    • Advanced guides
      • Containers guide
      • Application guide
      • Kubernetes guide
      • Create a cluster
      • Upgrade a cluster
      • Managing node groups
      • Patch node groups
      • Hostnames and rules
      • Serve traffic on multiple ports
      • Configure access to your K8s cluster
      • Designing for multiple environments
      • Microservices architecture
      • Monitoring your clusters
      • Performance tuning
      • Visibility and monitoring
      • Working with data
        • Container-based data
        • Seeding and migration
        • Cloud-provided data
        • Golden images
        • Third party
      • Pausing Instant Datasets
        • Application pausing schedules
        • Pause/resume environments
      • Infrastructure as code
        • Terraform
  • Reference documentation
    • Account settings
      • Account info
      • Managing users
      • Build settings
        • Build arguments
        • Build SSH keys
      • Add integrations
      • View clusters and cloud integrations
      • Add datasets
      • Environment handles
    • Workflows in Release
      • Stages of workflows
      • Serial deployments
      • Parallel deployments
      • Rolling deployments
      • Rainbow deployments
    • Networking
      • Network architecture (AWS)
      • Network architecture (GCP)
      • Ingresses
      • IP addresses
      • Cloud-provided services
      • Third-party services
    • Release environment versioning
    • Application settings
      • Application Template
        • Schema definition
      • Default environment variables
      • GitHub
      • Pull requests
      • GitOps
      • Just-in-time file mounts
      • Primary App Link
      • Create application FAQ
      • App-level build arguments
      • Parameters
      • Workspaces
    • End-to-end testing
    • Environment settings
      • Environment configuration
      • Environment variables
        • Environment variable mappings
        • Secrets vaults
        • Using Secrets with GitOps
        • Kubernetes Secrets as environment variables
        • Managing legacy Release Secrets
    • Environment expiration
    • Environment presets
    • Instant datasets on AWS
    • Instant datasets on GCP
    • Instant dataset tasks
      • Tonic Cloud
      • Tonic On-Premise
    • Cloud resources
    • Static service deployment
    • Helm
      • Getting started
      • Version-controlled Helm charts
      • Open-source charts
      • Building Docker images
      • Ingress and networking
      • Configuration
    • GitOps
    • The .release.yaml file
    • Docker Compose conversion support
    • Reference examples
      • Adding and removing services
      • Managing service resources
      • Adding database containers to the Application Template
      • Stock Off-The-Shelf Examples
    • Release API
      • Account Authentication
      • Environments API
        • Create
        • Get
        • Setup
        • Patch
      • User Authentication
      • Environment Presets API
        • Get Environment Preset List
        • Get Environment Preset
        • Put Environment Preset
  • Background concepts
    • How Release works
  • Frequently asked questions
    • Release FAQ
    • AWS FAQ
    • Docker FAQ
    • JavaScript FAQ
  • Integrations
    • Integrations overview
      • Artifactory integration
      • Cloud integrations (AWS)
        • AWS guides
        • Grant access to AWS resources
        • AWS how to increase EIP quota
        • Control your EKS fleet with systems manager
        • Managing STS access
        • AWS Permissions Boundaries
        • Private ECR Repositories
        • Using an Existing AWS VPC
        • Using an Existing EKS Cluster
      • Docker Hub integration
      • LaunchDarkly integration
      • Private registries
      • Slack integration
      • Cloud integrations (GCP)
        • GCP Permissions Boundary
      • Datadog Agent
      • Doppler Secrets Manager
      • AWS Secrets Management
    • Source control integrations
      • GitHub
        • Pull request comments
        • Pull request labels
        • GitHub deployments
        • GitHub statuses
        • Remove GitHub integration
      • Bitbucket
      • GitLab
    • Monitoring and logging add-ons
      • Datadog
      • New Relic
      • ELK (Elasticsearch, Logstash, and Kibana)
  • Release Delivery
    • Create new customer integration
    • Delivery guide
    • Release to customer account access controls
    • Delivery FAQs
  • Release Instant Datasets
    • Introduction
    • Quickstart
    • Security
      • AWS Instant Dataset security
    • FAQ
    • API
  • CLI
    • Getting started
    • Installation
    • Configuration
    • CLI usage example
    • Remote development environments
    • Command reference
      • release accounts
        • release accounts list
        • release accounts select
      • release ai
        • release ai chat
        • release ai config-delete
        • release ai config-init
        • release ai config-select
        • release ai config-upsert
      • release apps
        • release apps list
        • release apps select
      • release auth
        • release auth login
        • release auth logout
      • release builds
        • release builds create
      • release clusters
        • release clusters exec
        • release clusters kubeconfig
        • release clusters shell
      • release datasets
        • release datasets list
        • release datasets refresh
      • release deploys
        • release deploys create
        • release deploys list
      • release development
        • release development logs
        • release development start
      • release environments
        • release environments config-get
        • release environments config-set
        • release environments create
        • release environments delete
        • release environments get
        • release environments list
        • release environments vars-get
      • release gitops
        • release gitops init
        • release gitops validate
      • release instances
        • release instances exec
        • release instances logs
        • release instances terminal
  • Release.ai
    • Release.ai Introduction
    • Getting Started
    • Release.ai Templates
    • Template Configuration Basics
    • Using GPU Resources
    • Custom Workflows
    • Fine Tuning LlamaX
    • Serving Inference
Powered by GitBook
On this page
  • Overview
  • Fork and clone the repository
  • Project structure
  • Vote application
  • Worker application
  • Result application
  • Docker Compose
  • Run the project locally
  • Deploy to Release
  • Changing environment variables
  • Next steps

Was this helpful?

  1. Guides and examples
  2. Example applications

Full stack voting app

PreviousExample applicationsNextFlask and RDS counter app

Last updated 2 years ago

Was this helpful?

Overview

In this tutorial, we'll deploy a containerized full-stack application to Release. Our example voting application will allow users to vote for their favorite category and then view the results of the votes. We'll use multiple stacks and frameworks for our app to illustrate the breadth and flexibility of deployments in Release.

Our codebase will comprise three services that will be containerized and managed by a docker-compose file. Additionally, we will use Postgres as a database and Redis as a message broker to offload some of the computational load to the worker service. Our services will be:

  • Vote: A frontend and some server-side code that will push the vote made by a user to Redis. This will be built in Python, using the Flask framework.

  • Result: A frontend that uses a websocket API to poll data from its server-side implementation to provide real-time updates of votes. This will be a Node.js application that uses Express to serve an Angular frontend. The frontend will use Socket.IO to manage the websocket connection.

  • Worker: The background task processor that reads from Redis and creates entries in our Postgres database to represent the results of the votes. The worker will be implemented using Java.

Our completed voting application will look like this:

And the result application will look like this:

You can find the completed code for the project here.

Fork and clone the repository

Create a fork of the repository on your version-control hosting provider (GitHub, GitLab, or Bitbucket). Ensure that the provider you fork the repository to is the provider you have integrated with Release.

Once you have forked the repository, you can clone it to your development machine to get started.

You do not need to have the repository clone to deploy the application to Release, but it is useful to be able to work through the code and understand the codebase.

Project structure

Vote application

Our vote application is a small Flask web app that accepts POST requests from the index.html file it bundles and serves statically via a GET request to /.

If you take a look at the vote/app.py file, you should see the code below:

@app.route("/", methods=['POST','GET'])
 def hello():
     voter_id = request.cookies.get('voter_id')
     if not voter_id:
         voter_id = hex(random.getrandbits(64))[2:-1]

     vote = None

     if request.method == 'POST':
         redis = get_redis()
         vote = request.form['vote']
         data = json.dumps({'voter_id': voter_id, 'vote': vote})
         redis.rpush('votes', data)

     resp = make_response(render_template(
         'index.html',
         option_a=option_a,
         option_b=option_b,
         hostname=hostname,
         vote=vote,
     ))
     resp.set_cookie('voter_id', voter_id)
     return resp

This code has a few responsibilities:

  • When an HTTP request is received, we assign a voter ID to the caller, if one is not already present as a cookie on the request.

  • If the HTTP request is a POST request, we connect to Redis, and push a JSON payload containing voter data onto a Redis queue called votes.

  • If the HTTP request is a GET request, we return the index.html template file with a few parameters.

The most important parameters provided to our template are option_a and option_b.

option_a = os.getenv('OPTION_A', "Cats")
option_b = os.getenv('OPTION_B', "Dogs")

These are the categories that a user can vote for. If the environment variables for OPTION_A and OPTION_B aren’t set, the default options will be "Cats" and "Dogs".

Worker application

The worker application is purely a backend service, written in Java.

On startup, it establishes a connection to Redis and the PostgreSQL database.

...
class Worker {
  public static void main(String[] args) {
    try {
      Jedis redis = connectToRedis("redis");
      Connection dbConn = connectToDB("db");
...
		}
	}
}

As part of the connection to our database, the worker application also creates the necessary database tables.

...
PreparedStatement st = conn.prepareStatement(
        "CREATE TABLE IF NOT EXISTS votes (id VARCHAR(255) NOT NULL UNIQUE, vote VARCHAR(255) NOT NULL)");
      st.executeUpdate();
...

It then watches the Redis queue called votes for new items.

while (true) {
        String voteJSON = redis.blpop(0, "votes").get(1);
        JSONObject voteData = new JSONObject(voteJSON);
        String voterID = voteData.getString("voter_id");
        String vote = voteData.getString("vote");

        System.err.printf("Processing vote for '%s' by '%s'\n", vote, voterID);
        updateVote(dbConn, voterID, vote);
      }

When a new item is found, it calls a method called updateVote, which handles writing the result of a vote to the PostgreSQL database.

static void updateVote(Connection dbConn, String voterID, String vote) throws SQLException {
    PreparedStatement insert = dbConn.prepareStatement(
      "INSERT INTO votes (id, vote) VALUES (?, ?)");
    insert.setString(1, voterID);
    insert.setString(2, vote);

    try {
      insert.executeUpdate();
    } catch (SQLException e) {
      PreparedStatement update = dbConn.prepareStatement(
        "UPDATE votes SET vote = ? WHERE id = ?");
      update.setString(1, vote);
      update.setString(2, voterID);
      update.executeUpdate();
    }
  }

Result application

The result application, in a similar fashion to the vote application, serves an index.html file via its / route.

More interestingly, it exposes a websocket API using Socket.IO.

io.sockets.on('connection', function (socket) {

  socket.emit('message', { text : 'Welcome!' });

  socket.on('subscribe', function (data) {
    socket.join(data.channel);
  });
});

On startup, the result application establishes a connection to the PostgreSQL database.

async.retry(
  {times: 1000, interval: 1000},
  function(callback) {
    pool.connect(function(err, client, done) {
      if (err) {
        console.error("Waiting for db");
      }
      callback(err, client);
    });
  },
  function(err, client) {
    if (err) {
      return console.error("Giving up");
    }
    console.log("Connected to db");
    getVotes(client);
  }
);

Once a connection has been successfully established, it calls a getVotes() function using the database client.

This function reads the vote results from the database (which were written to it via the worker) and publishes them to a Socket.IO-managed channel called scores.

function getVotes(client) {
  client.query('SELECT vote, COUNT(id) AS count FROM votes GROUP BY vote', [], function(err, result) {
    if (err) {
      console.error("Error performing query: " + err);
    } else {
      var votes = collectVotesFromResult(result);
      io.sockets.emit("scores", JSON.stringify(votes));
    }

    setTimeout(function() {getVotes(client) }, 1000);
  });
}

Our client-side code (anchored at result/views/app.js) reads from the scores channel and updates the result application’s frontend accordingly.

...
var updateScores = function(){
    socket.on('scores', function (json) {
       data = JSON.parse(json);
       var a = parseInt(data.a || 0);
       var b = parseInt(data.b || 0);

       var percentages = getPercentages(a, b);

       bg1.style.width = percentages.a + "%";
       bg2.style.width = percentages.b + "%";

       $scope.$apply(function () {
         $scope.aPercent = percentages.a;
         $scope.bPercent = percentages.b;
         $scope.total = a + b;
       });
    });
  };
...

Docker Compose

Each of the applications described above is containerized using a dockerfile in their respective directories. We can use docker-compose to coordinate and run our applications together, as well as run containerized versions of Redis and PostgreSQL.

Below is the complete docker-compose.yml file required to build and run our applications.

version: "3"

services:
  vote:
    build: ./vote
    command:
      - python
      - app.py
    ports:
      - "5000:80"
    depends_on:
      - "redis"
      - "db"
  
  result:
    build: ./result
    command:
      - nodemon
      - server.js
    ports:
      - "5001:80"
    depends_on:
      - "redis"
      - "db"
  
  worker:
    build:
      context: ./worker
    depends_on:
      - "redis"
      - "db"
  
  redis:
    image: redis:alpine
    ports:
      - "6379"
    volumes:
      - redis:/data
  
  db:
    image: postgres:14
    ports:
      - "5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres

volumes:
  postgres-data: {}
  redis: {}

Run the project locally

To run our project locally, ensure you have Docker installed, and run the following command in the root of the project:

docker-compose up 

The vote application will be accessible via port 5001 on localhost and the Result application will be available via port 5002.

Deploy to Release

Once we’ve created the applications and set up our docker-compose.yaml file, we’re ready to deploy our app to Release.

Ensure you’ve forked the repository before we get started. The instructions to deploy our example voting app are here.

After deployment, we can click on the hostname URL for the vote application to tinker with making votes. You can share this URL with other people to vote, too.

To view the results in real-time, you can navigate to the hostname URL for the result application.

Changing environment variables

Our default environment variables for OPTION_A and OPTION_B were set to “Cats” and “Dogs”, but perhaps we’d like our users to choose between “Python” and “JavaScript”.

To modify this, we can navigate back to our Application Dashboard and click on our ephemeral environment.

From there click on the Settings tab and click the Edit button for the Environment Variables section.

From here, we can modify the environment variables for the environment. We will add two variables, for OPTION_A and OPTION_B respectively. Then click Save As New Version and then Apply.

This will apply the latest configuration changes to our live environment and redeploy it.

Once our deployment is complete, we should be able to navigate back to the vote application and see our new voting categories in action!

Next steps

In this tutorial, we’ve learned how to set up and deploy a non-trivial project with multiple services using Release. We’ve also looked at how to configure databases using Docker on Release. Additionally, we learned how to modify environment configurations and redeploy afterward.

A good next step might be to create an environment specifically for a development branch of the project so that you can iterate on your project without impacting a production deployment.