diff --git a/.dockerignore b/.dockerignore
index b5ecbf8..b9ff5d8 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -19,7 +19,9 @@ docker-compose*
 # Others
 .lgtm.yml
 .travis.yml
+.vscode
 
 # App data & bin
 db
-wireguard-ui
\ No newline at end of file
+assets
+wireguard-ui
diff --git a/.gitignore b/.gitignore
index 84cffa0..d6eba78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,12 @@ wireguard-ui
 # Output of the go coverage tool, specifically when used with LiteIDE
 *.out
 
-# Dependency directories (remove the comment below to include it)
+# Dependency directories and files (remove the comment below to include it)
 vendor/
 assets/
-node_modules
+node_modules/
+rice-box.go
+
+# IDEs
+.vscode
+.idea
diff --git a/Dockerfile b/Dockerfile
index 1010554..3d73d38 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,16 +12,19 @@ WORKDIR /build
 # Add sources
 COPY . /build
 
-# Get application dependencies and build
-RUN go mod download
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o wg-ui .
-
 # Prepare assets
 RUN yarn install --pure-lockfile --production && \
     yarn cache clean
 
+# Move admin-lte dist
+RUN mkdir -p assets/dist/js assets/dist/css && \
+    cp /build/node_modules/admin-lte/dist/js/adminlte.min.js \
+    assets/dist/js/adminlte.min.js && \
+    cp /build/node_modules/admin-lte/dist/css/adminlte.min.css \
+    assets/dist/css/adminlte.min.css
+
 # Move plugin assets
-RUN mkdir -p /assets/plugins && \
+RUN mkdir -p assets/plugins && \
     cp -r /build/node_modules/admin-lte/plugins/jquery/ \
     /build/node_modules/admin-lte/plugins/fontawesome-free/ \
     /build/node_modules/admin-lte/plugins/bootstrap/ \
@@ -30,8 +33,15 @@ RUN mkdir -p /assets/plugins && \
     /build/node_modules/admin-lte/plugins/jquery-validation/ \
     /build/node_modules/admin-lte/plugins/select2/ \
     /build/node_modules/jquery-tags-input/ \
-    /assets/plugins/
+    assets/plugins/
 
+# Get go modules and build tool
+RUN go mod download && \
+    go get github.com/GeertJohan/go.rice/rice
+
+# Build
+RUN rice embed-go && \
+    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o wg-ui .
 
 # Release stage
 FROM alpine:3.11
@@ -47,12 +57,6 @@ RUN mkdir -p db
 
 # Copy binary files
 COPY --from=builder --chown=wgui:wgui /build/wg-ui /app
-# Copy templates
-COPY --from=builder --chown=wgui:wgui /build/templates /app/templates
-# Copy assets
-COPY --from=builder --chown=wgui:wgui /build/node_modules/admin-lte/dist/js/adminlte.min.js /app/assets/dist/js/adminlte.min.js
-COPY --from=builder --chown=wgui:wgui /build/node_modules/admin-lte/dist/css/adminlte.min.css /app/assets/dist/css/adminlte.min.css
-COPY --from=builder --chown=wgui:wgui /assets/plugins /app/assets/plugins
 
 RUN chmod +x wg-ui
 
diff --git a/README.md b/README.md
index 22977aa..927150f 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,24 @@ A web user interface to manage your WireGuard setup.
 - Retrieve configs using QR code / file
 
 ## Run WireGuard-UI
-Only docker option for now, please refer to this example of [docker-compose.yml](https://github.com/ngoduykhanh/wireguard-ui/blob/master/docker-compose.yaml).
 
-Please adjust volume mount points to work with your setup. Then run it:
+Default username and password are `admin`.
+
+### Using docker compose
+
+You can take a look at this example of [docker-compose.yml](https://github.com/ngoduykhanh/wireguard-ui/blob/master/docker-compose.yaml). Please adjust volume mount points to work with your setup. Then run it like below:
 
 ```
 docker-compose up
 ```
 
-Default username and password are `admin`.
+### Using binary file
+
+Download the binary file from the release and run it with command:
+
+```
+./wireguard-ui
+```
 
 ## Auto restart WireGuard daemon
 WireGuard-UI only takes care of configuration generation. You can use systemd to watch for the changes and restart the service. Following is an example:
@@ -52,6 +61,38 @@ systemctl enable wgui.{path,service}
 systemctl start wgui.{path,service}
 ```
 
+## Build
+
+### Build docker image
+
+Go to the project root directory and run the following command:
+
+```
+docker build -t wireguard-ui .
+```
+
+### Build binary file
+
+Prepare the assets directory
+
+```
+./prepare_assets.sh
+```
+
+Then you can embed resources by generating Go source code
+
+```
+rice embed-go
+go build -o wireguard-ui
+```
+
+Or, append resources to executable as zip file
+
+```
+go build -o wireguard-ui
+rice append --exec wireguard-ui
+```
+
 ## Screenshot
 
 ![wireguard-ui](https://user-images.githubusercontent.com/6447444/80270680-76adf980-86e4-11ea-8ca1-9237f0dfa249.png)
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 72da096..d8ba0a1 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -2,7 +2,8 @@ version: '3'
 
 services:
   wg:
-    image: ngoduykhanh/wireguard-ui:latest
+    #image: ngoduykhanh/wireguard-ui:latest
+    image: wgui:latest
     container_name: wgui
     ports:
       - 5000:5000
@@ -12,4 +13,4 @@ services:
         max-size: 50m
     volumes:
       - ./db:/app/db
-      - /etc/wireguard:/etc/wireguard
+#      - /etc/wireguard:/etc/wireguard
diff --git a/go.mod b/go.mod
index 8ad8494..a932024 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/ngoduykhanh/wireguard-ui
 go 1.14
 
 require (
+	github.com/GeertJohan/go.rice v1.0.0
 	github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
 	github.com/go-playground/universal-translator v0.17.0 // indirect
 	github.com/gorilla/sessions v1.2.0
diff --git a/go.sum b/go.sum
index ec083b5..0bd16d2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,8 @@
+github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
+github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
+github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
@@ -7,6 +11,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
+github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
@@ -39,6 +45,7 @@ github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYb
 github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8=
 github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
 github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -75,6 +82,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/handler/routes.go b/handler/routes.go
index 7b0d689..274de90 100644
--- a/handler/routes.go
+++ b/handler/routes.go
@@ -3,6 +3,7 @@ package handler
 import (
 	"encoding/json"
 	"fmt"
+	rice "github.com/GeertJohan/go.rice"
 	"net/http"
 	"strings"
 	"time"
@@ -429,7 +430,7 @@ func SuggestIPAllocation() echo.HandlerFunc {
 }
 
 // ApplyServerConfig handler to write config file and restart Wireguard server
-func ApplyServerConfig() echo.HandlerFunc {
+func ApplyServerConfig(tmplBox *rice.Box) echo.HandlerFunc {
 	return func(c echo.Context) error {
 		// access validation
 		validSession(c)
@@ -453,7 +454,7 @@ func ApplyServerConfig() echo.HandlerFunc {
 		}
 
 		// Write config file
-		err = util.WriteWireGuardServerConfig(server, clients, settings)
+		err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings)
 		if err != nil {
 			log.Error("Cannot apply server config: ", err)
 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Cannot apply server config: %v", err)})
diff --git a/main.go b/main.go
index bf8d911..0d63700 100644
--- a/main.go
+++ b/main.go
@@ -2,10 +2,12 @@ package main
 
 import (
 	"fmt"
-
+	rice "github.com/GeertJohan/go.rice"
+	"github.com/labstack/echo/v4"
 	"github.com/ngoduykhanh/wireguard-ui/handler"
 	"github.com/ngoduykhanh/wireguard-ui/router"
 	"github.com/ngoduykhanh/wireguard-ui/util"
+	"net/http"
 )
 
 func main() {
@@ -15,8 +17,14 @@ func main() {
 		fmt.Print("Cannot init database: ", err)
 	}
 
+	// create rice box for embedded template
+	tmplBox := rice.MustFindBox("templates")
+
+	// rice file server for assets. "assets" is the folder where the files come from.
+	assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox())
+
 	// register routes
-	app := router.New()
+	app := router.New(tmplBox)
 
 	app.GET("/", handler.WireGuardClients())
 	app.GET("/login", handler.LoginPage())
@@ -33,6 +41,10 @@ func main() {
 	app.POST("/global-settings", handler.GlobalSettingSubmit())
 	app.GET("/api/machine-ips", handler.MachineIPAddresses())
 	app.GET("/api/suggest-client-ips", handler.SuggestIPAllocation())
-	app.GET("/api/apply-wg-config", handler.ApplyServerConfig())
+	app.GET("/api/apply-wg-config", handler.ApplyServerConfig(tmplBox))
+
+	// servers other static files
+	app.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
+
 	app.Logger.Fatal(app.Start("0.0.0.0:5000"))
 }
diff --git a/prepare_assets.sh b/prepare_assets.sh
new file mode 100755
index 0000000..801da8c
--- /dev/null
+++ b/prepare_assets.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -e
+
+DIR=$(dirname "$0")
+
+# install node modules
+yarn install --pure-lockfile --production
+
+# Copy admin-lte dist
+mkdir -p "${DIR}/assets/dist/js" "${DIR}/assets/dist/css" && \
+  cp -r "${DIR}/node_modules/admin-lte/dist/js/adminlte.min.js" "${DIR}/assets/dist/js/adminlte.min.js" && \
+  cp -r "${DIR}/node_modules/admin-lte/dist/css/adminlte.min.css" "${DIR}/assets/dist/css/adminlte.min.css"
+
+# Copy plugins
+mkdir -p "${DIR}/assets/plugins" && \
+  cp -r "${DIR}/node_modules/admin-lte/plugins/jquery" \
+  "${DIR}/node_modules/admin-lte/plugins/fontawesome-free" \
+  "${DIR}/node_modules/admin-lte/plugins/bootstrap" \
+  "${DIR}/node_modules/admin-lte/plugins/icheck-bootstrap" \
+  "${DIR}/node_modules/admin-lte/plugins/toastr" \
+  "${DIR}/node_modules/admin-lte/plugins/jquery-validation" \
+  "${DIR}/node_modules/admin-lte/plugins/select2" \
+  "${DIR}/node_modules/jquery-tags-input" \
+  "${DIR}/assets/plugins/"
diff --git a/router/router.go b/router/router.go
index de836f2..17ee70a 100644
--- a/router/router.go
+++ b/router/router.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"text/template"
 
+	"github.com/GeertJohan/go.rice"
 	"github.com/gorilla/sessions"
 	"github.com/labstack/echo-contrib/session"
 	"github.com/labstack/echo/v4"
@@ -32,15 +33,42 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c
 }
 
 // New function
-func New() *echo.Echo {
+func New(tmplBox *rice.Box) *echo.Echo {
 	e := echo.New()
 	e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret"))))
 
+	// read html template file to string
+	tmplBaseString, err := tmplBox.String("base.html")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tmplLoginString, err := tmplBox.String("login.html")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tmplClientsString, err := tmplBox.String("clients.html")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tmplServerString, err := tmplBox.String("server.html")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tmplGlobalSettingsString, err := tmplBox.String("global_settings.html")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// create template list
 	templates := make(map[string]*template.Template)
-	templates["login.html"] = template.Must(template.ParseFiles("templates/login.html"))
-	templates["clients.html"] = template.Must(template.ParseFiles("templates/clients.html", "templates/base.html"))
-	templates["server.html"] = template.Must(template.ParseFiles("templates/server.html", "templates/base.html"))
-	templates["global_settings.html"] = template.Must(template.ParseFiles("templates/global_settings.html", "templates/base.html"))
+	templates["login.html"] = template.Must(template.New("login").Parse(tmplLoginString))
+	templates["clients.html"] = template.Must(template.New("clients").Parse(tmplBaseString + tmplClientsString))
+	templates["server.html"] = template.Must(template.New("server").Parse(tmplBaseString + tmplServerString))
+	templates["global_settings.html"] = template.Must(template.New("global_settings").Parse(tmplBaseString + tmplGlobalSettingsString))
 
 	e.Logger.SetLevel(log.DEBUG)
 	e.Pre(middleware.RemoveTrailingSlash())
@@ -51,7 +79,6 @@ func New() *echo.Echo {
 		AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE},
 	}))
 	e.Validator = NewValidator()
-	e.Static("/static", "assets")
 	e.Renderer = &TemplateRegistry{
 		templates: templates,
 	}
diff --git a/util/util.go b/util/util.go
index a0dd0ac..7685ad5 100644
--- a/util/util.go
+++ b/util/util.go
@@ -10,6 +10,7 @@ import (
 	"text/template"
 	"time"
 
+	rice "github.com/GeertJohan/go.rice"
 	externalip "github.com/glendc/go-external-ip"
 	"github.com/ngoduykhanh/wireguard-ui/model"
 	"github.com/sdomino/scribble"
@@ -268,7 +269,7 @@ func GetAvailableIP(cidr string, allocatedList []string) (string, error) {
 		}
 	}
 
-	return "", errors.New("No more available ip address")
+	return "", errors.New("no more available ip address")
 }
 
 // ValidateIPAllocation to validate the list of client's ip allocation
@@ -312,12 +313,20 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
 }
 
 // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
-func WriteWireGuardServerConfig(serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error {
-	t, err := template.ParseFiles("templates/wg.conf")
+func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error {
+	// read wg.conf template file to string
+	tmplWireguardConf, err := tmplBox.String("wg.conf")
 	if err != nil {
 		return err
 	}
 
+	// parse the template
+	t, err := template.New("wg_config").Parse(tmplWireguardConf)
+	if err != nil {
+		return err
+	}
+
+	// write config file to disk
 	f, err := os.Create(globalSettings.ConfigFilePath)
 	if err != nil {
 		return err