Our server is finished and almost ready for deploy, which will be done using Docker. Notice that i said it's almost ready, so let's see what is missing. This whole time we used React development server which is listening on port 3000 and redirecting all requests to our backend on port 8080. That makes sense for development since it makes it easier to develop frontend and backend at the same time, as well as debugging frontend React app. But in production we don't need that and it makes much more sense to have only our backend server running and to serve static frontend files to client. So instead of starting React development server using npm start
command, we will build optimized frontend files for production, using command npm run build
inside of assets/
directory. This will create new directory assets/build/
along with all files needed for production. Now we also have to instruct our backend where to find these files to be able to serve them. That is simply done using command router.Use(static.Serve("/", static.LocalFile("./assets/build", true)))
. Of course, we want to do that only if server is started in prod
environment, so we need to slightly update few files.
First, we will update Parse()
function in internal/cli/cli.go
to return environment value as a string:
func Parse() string {
flag.Usage = usage
env := flag.String("env", "dev", `Sets run environment. Possible values are "dev" and "prod"`)
flag.Parse()
logging.ConfigureLogger(*env)
if *env == "prod" {
logging.SetGinLogToFile()
}
return *env
}
Then we will update Config
struct NewConfig()
function to be able to receive and set environment value:
type Config struct {
Host string
Port string
DbHost string
DbPort string
DbName string
DbUser string
DbPassword string
JwtSecret string
Env string
}
func NewConfig(env string) Config {
...
return Config{
Host: host,
Port: port,
DbHost: dbHost,
DbPort: dbPort,
DbName: dbName,
DbUser: dbUser,
DbPassword: dbPassword,
JwtSecret: jwtSecret,
Env: env,
}
}
Now we can update internal/cli/main.go
to receive env value form CLI, and send it to new configuration creation which will be used for starting server:
func main() {
env := cli.Parse()
server.Start(conf.NewConfig(env))
}
Next thing we have to do is update router to be able to receive configuration argument, and set it to serve static files if started in production mode:
package server
import (
"net/http"
"rgb/internal/conf"
"rgb/internal/store"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
func setRouter(cfg conf.Config) *gin.Engine {
// Creates default gin router with Logger and Recovery middleware already attached
router := gin.Default()
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
router.RedirectTrailingSlash = true
// Serve static files to frontend if server is started in production environment
if cfg.Env == "prod" {
router.Use(static.Serve("/", static.LocalFile("./assets/build", true)))
}
// Create API route group
api := router.Group("/api")
api.Use(customErrors)
{
api.POST("/signup", gin.Bind(store.User{}), signUp)
api.POST("/signin", gin.Bind(store.User{}), signIn)
}
authorized := api.Group("/")
authorized.Use(authorization)
{
authorized.GET("/posts", indexPosts)
authorized.POST("/posts", gin.Bind(store.Post{}), createPost)
authorized.PUT("/posts", gin.Bind(store.Post{}), updatePost)
authorized.DELETE("/posts/:id", deletePost)
}
router.NoRoute(func(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{}) })
return router
}
Last line needing update is in migrations/main.go
file. Just change
store.SetDBConnection(database.NewDBOptions(conf.NewConfig()))
to
store.SetDBConnection(database.NewDBOptions(conf.NewConfig("dev")))
Actually, that's not last thing. You will also have to update all tests that use configuration and router setup, but that's entirely up to you and it's dependent on what tests you have implemented.
Now everything is ready for Docker deploy. Docker is not in the scope of this guide, so i will not go into details about Dockerfile, .dockerignore and docker-compose.yml
contents.
First we will create .dockerignore
file in project root directory:
# This file
.dockerignore
# Git files
.git/
.gitignore
# VS Code config dir
.vscode/
# Docker configuration files
docker/
# Assets dependencies and built files
assets/build/
assets/node_modules/
# Log files
logs/
# Built binary
cmd/rgb/rgb
# ENV file
.env
# Readme file
README.md
Now create new directory docker/
with two files, Dockerfile and docker-compose.yml
. Content of Dockerfile
will be:
FROM node:16 AS frontendBuilder
# set app work dir
WORKDIR /rgb
# copy assets files to the container
COPY assets/ .
# set assets/ as work dir to build frontend static files
WORKDIR /rgb/assets
RUN npm install
RUN npm run build
FROM golang:1.16.3 AS backendBuilder
# set app work dir
WORKDIR /go/src/rgb
# copy all files to the container
COPY . .
# build app executable
RUN CGO_ENABLED=0 GOOS=linux go build -o cmd/rgb/rgb cmd/rgb/main.go
# build migrations executable
RUN CGO_ENABLED=0 GOOS=linux go build -o migrations/migrations migrations/*.go
FROM alpine:3.14
# Create a group and user deploy
RUN addgroup -S deploy && adduser -S deploy -G deploy
ARG ROOT_DIR=/home/deploy/rgb
WORKDIR ${ROOT_DIR}
RUN chown deploy:deploy ${ROOT_DIR}
# copy static assets file from frontend build
COPY --from=frontendBuilder --chown=deploy:deploy /rgb/build ./assets/build
# copy app and migrations executables from backend builder
COPY --from=backendBuilder --chown=deploy:deploy /go/src/rgb/migrations/migrations ./migrations/
COPY --from=backendBuilder --chown=deploy:deploy /go/src/rgb/cmd/rgb/rgb .
# set user deploy as current user
USER deploy
# start app
CMD [ "./rgb", "-env", "prod" ]
And content of docker-compose.yml
is:
version: "3"
services:
rgb:
image: kramat/rgb
env_file:
- ../.env
environment:
RGB_DB_HOST: db
depends_on:
- db
ports:
- ${RGB_PORT}:${RGB_PORT}
db:
image: postgres
environment:
POSTGRES_USER: ${RGB_DB_USER}
POSTGRES_PASSWORD: ${RGB_DB_PASSWORD}
POSTGRES_DB: ${RGB_DB_NAME}
ports:
- ${RGB_DB_PORT}:${RGB_DB_PORT}
volumes:
- postgresql:/var/lib/postgresql/rgb
- postgresql_data:/var/lib/postgresql/rgb/data
volumes:
postgresql: {}
postgresql_data: {}
All files required for Docker deploy are now ready, so let's see how to build Docker image and deploy it. First we will pull postgres
image from official Docker containers repository:
docker pull postgres
Next step is to build rgb
image. Inside of project root directory run (change DOCKER_ID
with your own docker ID):
docker build -t DOCKER_ID/rgb -f docker/Dockerfile .
To create rgb
and db
containers with resources, run:
cd docker/
docker-compose up -d
That will start both containers, and you can check their status by running docker ps
. Finally, we need to run migrations. Open shell in rgb
container by running:
docker-compose run --rm rgb sh
Inside of container we are able to run migrations same as before:
cd migrations/
./migrations init
./migrations up
And we are finished. You can open localhost:8080
in your browser to check that everything is working as it should, which means you should be able to create account and add new post:
It was quite long for a simple web app, but we have completed this guide. Hopefully it was useful to some of you. If you have any questions, remarks, or you have found any issues, feel free to contact me in comments. Good luck to everyone and happy coding :)
Top comments (0)