Fullstack application in Go with connection to PostgreSQL

To deploy your main application in Amverum, you need to follow these simple steps

  1. Open the page https://cloud.amverum.com/projects

  2. Click the Create button and select service type application

  3. We upload all the files (you can use git, or you can use the interface). Make sure you have downloaded all the necessary files. For the example project this is:

  • go.mod (necessarily)

  • go.sum (necessarily)

  • main.go (necessarily)

  • Dockerfile (necessarily)

  • static/index.html (if you use)

  • static/script.js (if you use)

  • static/styles.css (if you use)

  • amverum.yml (optional)

  1. After this, the build and deployment application will begin. Wait for the “Successfully Deployed” status to appear.

Let’s take a closer look at the process.

Let’s create a simple web application in the Go programming language, where you can leave and read quotes. To store them we will use the PostgreSQL DBMS.

The application directory has the following structure:

└─ code/
├── static
│   ├── styles.css
│   ├── script.js
│   └── index.html
├── amverum.yml
├── main.go
├── go.sum
├── go.mod
└── Dockerfile 

The code for static files is available at the end of the page.

Configuration

Amverum.yaml

You can write a yaml file yourself, or fill it out in the “Configuration” section of your personal account.

Example amverum.yml file when using only amverum.yml:

meta:
  environment: golang
  toolchain:
    name: go
    version: 1.22
build:
  image: golang:1.22
run:
  image: golang:1.22
  persistenceMount: /data
  containerPort: 80

Example amverum.yml file when used with Dockerfile:

meta:
  environment: docker
  toolchain: docker
build:
  dockerfile: Dockerfile
  skip: false
run:
  persistenceMount: /data
  containerPort: "80"

Important: save modified files (database, etc.) to the permanent storage /data. This will avoid their loss during reassembly. The Data folder in the code and the permanent storage /data are different directories.

Let’s consider an alternative way to set the configuration - through a Dockerfile.

Dockerfile

Note: if you are using a Dockerfile, then in most cases you do not need to add the amverum.yml configuration file.

Steps:

  1. Create a Dockerfile in the project directory.

  2. In the Dockerfile we specify the base image called builder: FROM golang:1.21.1 AS builder

instead of 1.21.1 you can specify any other version that you need.

  1. Set the working directory: WORKDIR /app

  2. Copy the files main.go, go.mod and go.sum to the current directory of the working directory: COPY main.go go.mod go.sum ./

  3. We build the project and create the executable file server: RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o server

  4. We use the Alpine Linux image (this will reduce the size of the final image and improve its performance): FROM alpine:latest

  5. Copy the server executable file, compiled in the previous image, to the current directory of the working directory: COPY --from=builder /app/server ./

  6. Copy static files inside the container: COPY static/ ./static/

  7. Open port 80 for external connections: EXPOSE 80

  8. Add a command to launch the application: CMD ["./server", "--port", "80"]

The resulting Dockerfile:

FROM golang:1.21.1 AS builder

WORKDIR /app

COPY main.go go.mod go.sum ./

RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o server

FROM alpine:latest

COPY --from=builder /app/server ./

COPY static/ ./static/

EXPOSE 80

CMD ["./server", "--port", "80"]

Dependencies (go.mod and sum.go)

We initialize a new module for managing dependencies. To do this, run the following command, which will create the go.mod file:

$ go mod init main

Note: instead of main you can write an arbitrary line

All that remains is to run one more command, which will add two entries for each dependency and create a go.sum file:

go mod tidy

DBMS deployment(PostgreSQL)

The database needs to be deployed as a separate application, and then you can connect to it from the main application. Detailed instructions are available at link.

Creating a project in Amverum

The last step is to deploy the application itself. The main.go file contains the main code and connects to the database. Don’t forget to change the parameters for connecting to the database to those that you used in the previous step when creating the database on Amverum:

  • user = Username

  • password = User password

  • dbname = Name of the database being created

  • the host parameter can be found on the Info page of your PostgreSQL project (for example, amverum-username-cnpg-appname-rw)

main.go:

package main

import (
	"database/sql"
	"encoding/json"
	"log"
	"net/http"
	"strconv"

	_ "github.com/lib/pq"
)

// Specify the values that you specified when creating the database on Amverum Cloud
const (
	host     = "amverum-nskripko-cnpg-godb-rw"
	port     = 5432
	user     = "nick"
	password = "href239"
	dbname   = "godb"
)

func main() {
	portStr := strconv.Itoa(port)
	dbinfo := "host=" + host + " port=" + portStr + " user=" + user + " password=" + password + " dbname=" + dbname + " sslmode=disable"

	db, err := sql.Open("postgres", dbinfo)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	_, err = db.Exec(`CREATE TABLE IF NOT EXISTS quotes (
                        id SERIAL PRIMARY KEY,
                        quote TEXT NOT NULL
                    )`)
	if err != nil {
		log.Fatal(err)
	}
	
	http.HandleFunc("/quotes", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		rows, err := db.Query("SELECT quote FROM quotes")
		if err != nil {
			log.Println("Error querying database:", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var quotes []string
		for rows.Next() {
			var quote string
			if err := rows.Scan(&quote); err != nil {
				log.Println("Error scanning rows:", err)
				continue
			}
			quotes = append(quotes, quote)
		}

		json.NewEncoder(w).Encode(quotes)
	})

	http.HandleFunc("/addquote", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		var data struct {
			Quote string `json:"quote"`
		}
		if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
			http.Error(w, "Bad request", http.StatusBadRequest)
			return
		}

		_, err := db.Exec("INSERT INTO quotes (quote) VALUES ($1)", data.Quote)
		if err != nil {
			log.Println("Error inserting quote into database:", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		w.WriteHeader(http.StatusCreated)
	})

	fs := http.FileServer(http.Dir("static"))
	http.Handle("/", fs)

	log.Fatal(http.ListenAndServe(":80", nil))
}

:warning: The code is a demo example and we strongly do not recommend specifying the login and password for connecting to the database in the code. Use environment variables (secrets)!

Functionality check

  1. Go to the project settings and activate the domain name.

  2. Now you can go to this URL and our application will open.

If something does not work, we recommend that you read the Build and Application logs.

Congratulations, you have successfully created your first application in Amverum!

Code of static files from the example

static/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Quote Board</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>Quote Board</h1>
        <form id="quoteForm">
            <input type="text" id="quoteInput" placeholder="Enter your quote" required>
            <button type="submit">Submit</button>
        </form>
        <div id="quoteList"></div>
    </div>
    <script src="script.js"></script>
</body>
</html>

static/styles.css:

body {
    font-family: Arial, sans-serif;
}

.container {
    max-width: 600px;
    margin: 50px auto;
    padding: 0 20px;
}

input[type="text"] {
    width: calc(100% - 80px);
    padding: 10px;
    margin-right: 10px;
}

button {
    padding: 10px 20px;
    background-color: #007bff;
    color: #fff;
    border: none;
    cursor: pointer;
}

button:hover {
    background-color: #0056b3;
}

#quoteList {
    margin-top: 20px;
}

static/script.js:

document.addEventListener('DOMContentLoaded', () => {
    const quoteForm = document.getElementById('quoteForm');
    const quoteInput = document.getElementById('quoteInput');
    const quoteList = document.getElementById('quoteList');

    // Function to fetch quotes from the server and display them
    const fetchQuotes = async () => {
        try {
            const response = await fetch('/quotes');
            const quotes = await response.json();

            // Clear previous quotes
            quoteList.innerHTML = '';

            // Append new quotes to the list
            quotes.forEach(quote => {
                const quoteItem = document.createElement('div');
                quoteItem.textContent = quote;
                quoteList.appendChild(quoteItem);
            });
        } catch (error) {
            console.error('Error fetching quotes:', error);
        }
    };

    // Fetch initial quotes when the page loads
    fetchQuotes();

    // Submit quote form
    quoteForm.addEventListener('submit', async event => {
        event.preventDefault();
        const newQuote = quoteInput.value.trim();

        if (newQuote === '') {
            alert('Please enter a quote.');
            return;
        }

        try {
            // Send the new quote to the server
            await fetch('/addquote', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ quote: newQuote })
            });

            // Clear the input field
            quoteInput.value = '';

            // Fetch and display updated quotes
            fetchQuotes();
        } catch (error) {
            console.error('Error adding quote:', error);
        }
    });
});

Important

Save database files and other changeable data to permanent storage to avoid losing them when updating the project when the code folder is “rolled back” to the state of the repository update. The data folder in the project root and the /data directory are different directories.

You can check that the save is going to /data by going to the “data” folder on the “Repository” page.

Important

To avoid the 502 error, change host 127.0.0.1 (or similar localhost) to 0.0.0.0 in your code, and specify in the configuration the port that your application listens to (example - 8080).

If you are unable to deploy the project

Write the symptoms you observe to support@amverum.com indicating your username and project name, we will try to help you.