> For the complete documentation index, see [llms.txt](https://docs.release.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.release.com/guides-and-examples/common-setup-examples/example-voting-app.md).

# Full stack voting app

## 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](http://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.

![](https://585411240-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M1neGLLQ0sDXeK6ooSo%2Fuploads%2Fgit-blob-c33e6a5de3b1fd0f6881556455e4f888347600eb%2F1-architecture-voting-app.png?alt=media\&token=d397d299-ea09-4459-a8fb-52a0897677b7)

Our completed voting application will look like this:

![](https://585411240-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M1neGLLQ0sDXeK6ooSo%2Fuploads%2Fgit-blob-44b68a40c89b3ad9b2a4275d274fad482fac8032%2F2-screenshot-voting-app.png?alt=media\&token=bf61889d-6433-4c6c-b175-5e1757e78f3d)

And the result application will look like this:

![](https://585411240-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M1neGLLQ0sDXeK6ooSo%2Fuploads%2Fgit-blob-d28d57f7ef3f30cb955482bfb51467a49242de31%2F3-screenshot-voting-app.png?alt=media\&token=250c5186-5725-4d7e-b2f0-6f8077a3d247)

You can find the completed code for the project [here](https://github.com/awesome-release/release-example-voting-app).

## Fork and clone the repository

Create a fork of the [repository](https://github.com/awesome-release/release-example-voting-app) 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](https://docs.releasehub.com/integrations/source-control-integrations).

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

{% hint style="info" %}
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.
{% endhint %}

## 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:

```jsx
@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`.

```jsx
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.

```jsx
...
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.

```jsx
...
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.

```jsx
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.

```jsx
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](http://socket.io).

```jsx
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.

```jsx
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](http://socket.io)-managed channel called `scores`.

```jsx
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.

```jsx
...
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.

```yaml
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:

```jsx
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](https://docs.release.com/getting-started/create-an-application).

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.

![](https://585411240-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M1neGLLQ0sDXeK6ooSo%2Fuploads%2Fgit-blob-b9f5a7ed7dd03471f6a9b85c2a6228dd95abb69f%2F14-config-voting-app.png?alt=media\&token=33c5999d-8796-4863-a1a7-350fb4b7bc59)

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

![](https://585411240-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M1neGLLQ0sDXeK6ooSo%2Fuploads%2Fgit-blob-d1d8e73bf8c55b214b3aeb9c304908763317f3b9%2F15-env-vars-voting-app.png?alt=media\&token=2d399d52-8bee-47a2-b06a-b33f9c8cd768)

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**.

![](https://585411240-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M1neGLLQ0sDXeK6ooSo%2Fuploads%2Fgit-blob-eeacba962adb3f6cd50765d4017486cbf0e6fcbd%2F16-env-config-voting-app.png?alt=media\&token=50b857b7-3300-4cbd-acd3-3f243f2dc7ac)

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!

![](https://585411240-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M1neGLLQ0sDXeK6ooSo%2Fuploads%2Fgit-blob-bef6c55e54f5bbd13317ba5339c59e1fbb29dd6e%2F17-updated-voting-app.png?alt=media\&token=ff8f2bce-ebcb-416b-87c3-85d390a17d16)

### 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.release.com/guides-and-examples/common-setup-examples/example-voting-app.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
