diff --git a/.dockerignore b/.dockerignore
index 4ac6094..ea93f80 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,6 +2,8 @@
 .gitignore
 .prettierrc
 .vscode
+.env
+example.env
 photos_path
 screenshots
 
diff --git a/Dockerfile b/Dockerfile
index 93c9f06..6b1c793 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,8 @@
 # Build UI
 FROM node:10 as ui
 
-ARG GRAPHQL_ENDPOINT
-ENV GRAPHQL_ENDPOINT=${GRAPHQL_ENDPOINT}
+ARG API_ENDPOINT
+ENV API_ENDPOINT=${API_ENDPOINT}
 
 RUN mkdir -p /app
 WORKDIR /app
diff --git a/api/example.env b/api/example.env
index 2985579..86b0e78 100644
--- a/api/example.env
+++ b/api/example.env
@@ -2,9 +2,17 @@
 
 MYSQL_URL=user:password@tcp(localhost)/dbname
 
-API_ENDPOINT=http://localhost:4001/
+API_LISTEN_IP=localhost
+API_LISTEN_PORT=4001
 
-PUBLIC_ENDPOINT=http://localhost:1234/
+# The url from which the server can be accessed publicly
+API_ENDPOINT=http://localhost:4001/
+UI_ENDPOINT=http://localhost:1234/
+
+# Set to 1 for the server to also serve the built static ui files
+SERVE_UI=0
+# When SERVE_UI is 1, PUBLIC_ENDPOINT is used instead of API_ENDPOINT and UI_ENDPOINT
+#PUBLIC_ENDPOINT=http://localhost:4001/
 
 # Set to 1 to set server in development mode, this enables graphql playground
 # Remove this if running in production
diff --git a/api/graphql/models/photo.go b/api/graphql/models/photo.go
index a649233..dc6eb24 100644
--- a/api/graphql/models/photo.go
+++ b/api/graphql/models/photo.go
@@ -2,10 +2,9 @@ package models
 
 import (
 	"database/sql"
-	"log"
-	"net/url"
-	"os"
 	"path"
+
+	"github.com/viktorstrate/photoview/api/utils"
 )
 
 type Photo struct {
@@ -66,18 +65,9 @@ func NewPhotosFromRows(rows *sql.Rows) ([]*Photo, error) {
 
 func (p *PhotoURL) URL() string {
 
-	publicUrl := os.Getenv("PUBLIC_ENDPOINT")
-	if publicUrl == "" {
-		publicUrl = os.Getenv("API_ENDPOINT")
-	}
+	imageUrl := utils.ApiEndpointUrl()
+	imageUrl.Path = path.Join(imageUrl.Path, "photo", p.PhotoName)
 
-	imageUrl, err := url.Parse(publicUrl)
-	if err != nil {
-		log.Println("Endpoint url is not properly configured, make sure the PUBLIC_ENDPOINT AND API_ENDPOINT environment variables are set correctly")
-		return p.PhotoName
-	}
-
-	imageUrl.Path = path.Join(imageUrl.Path, "api", "photo", p.PhotoName)
 	return imageUrl.String()
 }
 
diff --git a/api/server.go b/api/server.go
index 04bca0a..b0e8310 100644
--- a/api/server.go
+++ b/api/server.go
@@ -3,7 +3,6 @@ package main
 import (
 	"log"
 	"net/http"
-	"net/url"
 	"os"
 	"path"
 	"path/filepath"
@@ -16,14 +15,13 @@ import (
 	"github.com/viktorstrate/photoview/api/graphql/auth"
 	"github.com/viktorstrate/photoview/api/routes"
 	"github.com/viktorstrate/photoview/api/server"
+	"github.com/viktorstrate/photoview/api/utils"
 
 	"github.com/99designs/gqlgen/handler"
 	photoview_graphql "github.com/viktorstrate/photoview/api/graphql"
 	"github.com/viktorstrate/photoview/api/graphql/resolvers"
 )
 
-const defaultPort = "4001"
-
 // spaHandler implements the http.Handler interface, so we can use it
 // to respond to HTTP requests. The path to the static directory and
 // path to the index file within that static directory are used to
@@ -98,23 +96,19 @@ func main() {
 		Directives: graphqlDirective,
 	}
 
-	endpointURL, err := url.Parse(os.Getenv("API_ENDPOINT"))
-	if err != nil {
-		log.Println("WARN: Environment variable API_ENDPOINT not specified")
-		endpointURL, _ = url.Parse("/")
-	}
+	apiListenUrl := utils.ApiListenUrl()
 
-	endpointRouter := rootRouter.PathPrefix(endpointURL.Path).Subrouter()
+	endpointRouter := rootRouter.PathPrefix(apiListenUrl.Path).Subrouter()
 
 	if devMode {
-		endpointRouter.Handle("/api", handler.Playground("GraphQL playground", path.Join(endpointURL.Path, "/graphql")))
+		endpointRouter.Handle("/", handler.Playground("GraphQL playground", path.Join(apiListenUrl.Path, "/graphql")))
 	} else {
-		endpointRouter.HandleFunc("/api", func(w http.ResponseWriter, req *http.Request) {
+		endpointRouter.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
 			w.Write([]byte("photoview api endpoint"))
 		})
 	}
 
-	endpointRouter.Handle("/api/graphql",
+	endpointRouter.Handle("/graphql",
 		handler.GraphQL(photoview_graphql.NewExecutableSchema(graphqlConfig),
 			handler.IntrospectionEnabled(devMode),
 			handler.WebsocketUpgrader(server.WebsocketUpgrader(devMode)),
@@ -122,22 +116,28 @@ func main() {
 		),
 	)
 
-	photoRouter := endpointRouter.PathPrefix("/api/photo").Subrouter()
+	photoRouter := endpointRouter.PathPrefix("/photo").Subrouter()
 	routes.RegisterPhotoRoutes(db, photoRouter)
 
-	spa := spaHandler{staticPath: "/ui", indexPath: "index.html"}
-	endpointRouter.PathPrefix("/").Handler(spa)
+	shouldServeUI := os.Getenv("SERVE_UI") == "1"
 
-	if devMode {
-		log.Printf("🚀 Graphql playground ready at %s\n", endpointURL.String())
-	} else {
-		log.Printf("Photoview API endpoint listening at %s\n", endpointURL.String())
-
-		publicEndpoint := os.Getenv("PUBLIC_ENDPOINT")
-		if publicEndpoint != "" && publicEndpoint != endpointURL.String() {
-			log.Printf("Photoview API public endpoint ready at %s\n", publicEndpoint)
-		}
+	if shouldServeUI {
+		spa := spaHandler{staticPath: "/ui", indexPath: "index.html"}
+		rootRouter.PathPrefix("/").Handler(spa)
 	}
 
-	log.Fatal(http.ListenAndServe(":"+endpointURL.Port(), rootRouter))
+	if devMode {
+		log.Printf("🚀 Graphql playground ready at %s\n", apiListenUrl.String())
+	} else {
+		log.Printf("Photoview API endpoint listening at %s\n", apiListenUrl.String())
+
+		uiEndpoint := utils.UiEndpointUrl()
+		apiEndpoint := utils.ApiEndpointUrl()
+
+		log.Printf("Photoview API public endpoint ready at %s\n", apiEndpoint.String())
+		log.Printf("Photoview UI public endpoint ready at %s\n", uiEndpoint.String())
+
+	}
+
+	log.Fatal(http.ListenAndServe(":"+apiListenUrl.Port(), rootRouter))
 }
diff --git a/api/server/cors_middleware.go b/api/server/cors_middleware.go
index e131b0d..37e185d 100644
--- a/api/server/cors_middleware.go
+++ b/api/server/cors_middleware.go
@@ -1,14 +1,12 @@
 package server
 
 import (
-	"log"
 	"net/http"
-	"net/url"
-	"os"
 	"path"
 	"strings"
 
 	"github.com/gorilla/mux"
+	"github.com/viktorstrate/photoview/api/utils"
 )
 
 func CORSMiddleware(devMode bool) mux.MiddlewareFunc {
@@ -22,10 +20,7 @@ func CORSMiddleware(devMode bool) mux.MiddlewareFunc {
 			w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
 			w.Header().Set("Access-Control-Expose-Headers", "content-length")
 
-			endpoint, err := url.Parse(os.Getenv("API_ENDPOINT"))
-			if err != nil {
-				log.Fatalln("Could not parse API_ENDPOINT environment variable as url")
-			}
+			endpoint := utils.ApiEndpointUrl()
 			endpoint.Path = path.Join(endpoint.Path, "graphql")
 
 			if devMode {
@@ -34,12 +29,8 @@ func CORSMiddleware(devMode bool) mux.MiddlewareFunc {
 				w.Header().Set("Vary", "Origin")
 			} else {
 				// Production environment
-				publicEndpoint, err := url.Parse(os.Getenv("PUBLIC_ENDPOINT"))
-				if err != nil {
-					log.Printf("Error parsing environment variable PUBLIC_ENDPOINT as url: %s", err)
-				} else {
-					w.Header().Set("Access-Control-Allow-Origin", publicEndpoint.Scheme+"://"+publicEndpoint.Host)
-				}
+				uiEndpoint := utils.UiEndpointUrl()
+				w.Header().Set("Access-Control-Allow-Origin", uiEndpoint.Scheme+"://"+uiEndpoint.Host)
 			}
 
 			if req.Method != http.MethodOptions {
diff --git a/api/server/websocket.go b/api/server/websocket.go
index 99043b4..3786d36 100644
--- a/api/server/websocket.go
+++ b/api/server/websocket.go
@@ -4,9 +4,9 @@ import (
 	"log"
 	"net/http"
 	"net/url"
-	"os"
 
 	"github.com/gorilla/websocket"
+	"github.com/viktorstrate/photoview/api/utils"
 )
 
 func WebsocketUpgrader(devMode bool) websocket.Upgrader {
@@ -15,11 +15,7 @@ func WebsocketUpgrader(devMode bool) websocket.Upgrader {
 			if devMode {
 				return true
 			} else {
-				pubEndpoint, err := url.Parse(os.Getenv("PUBLIC_ENDPOINT"))
-				if err != nil {
-					log.Printf("Could not parse API_ENDPOINT environment variable as url: %s", err)
-					return false
-				}
+				uiEndpoint := utils.UiEndpointUrl()
 
 				if r.Header.Get("origin") == "" {
 					return true
@@ -31,10 +27,10 @@ func WebsocketUpgrader(devMode bool) websocket.Upgrader {
 					return false
 				}
 
-				if pubEndpoint.Host == originURL.Host {
+				if uiEndpoint.Host == originURL.Host {
 					return true
 				} else {
-					log.Printf("Not allowing websocket request from %s because it doesn't match PUBLIC_ENDPOINT %s", originURL.Host, pubEndpoint.Host)
+					log.Printf("Not allowing websocket request from %s because it doesn't match UI_ENDPOINT %s", originURL.Host, uiEndpoint.Host)
 					return false
 				}
 			}
diff --git a/api/utils/Endpoints.go b/api/utils/Endpoints.go
new file mode 100644
index 0000000..2179a6a
--- /dev/null
+++ b/api/utils/Endpoints.go
@@ -0,0 +1,82 @@
+package utils
+
+import (
+	"fmt"
+	"log"
+	"net/url"
+	"os"
+	"path"
+	"strconv"
+)
+
+func ApiListenUrl() *url.URL {
+	const defaultPort = "4001"
+
+	shouldServeUI := os.Getenv("SERVE_UI") == "1"
+
+	apiPrefix := "/"
+	if shouldServeUI {
+		apiPrefix = "/api"
+	}
+
+	var listenAddr string
+
+	listenAddr = os.Getenv("API_LISTEN_IP")
+	if listenAddr == "" {
+		listenAddr = "127.0.0.1"
+	}
+
+	listenPortStr := os.Getenv("API_LISTEN_PORT")
+	if listenPortStr == "" {
+		listenPortStr = defaultPort
+	}
+
+	listenPort, err := strconv.Atoi(listenPortStr)
+	if err != nil {
+		log.Fatalf("API_LISTEN_PORT must be a number: '%s'\n%s", listenPortStr, err)
+	}
+
+	apiUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", listenAddr, listenPort))
+	if err != nil {
+		log.Fatalf("Could not format api url: %s", err)
+	}
+	apiUrl.Path = apiPrefix
+
+	return apiUrl
+}
+
+func ApiEndpointUrl() *url.URL {
+	apiEndpointStr := os.Getenv("API_ENDPOINT")
+
+	shouldServeUI := os.Getenv("SERVE_UI") == "1"
+	if shouldServeUI {
+		apiEndpointStr = os.Getenv("PUBLIC_ENDPOINT")
+	}
+
+	apiEndpointUrl, err := url.Parse(apiEndpointStr)
+	if err != nil {
+		log.Fatalf("ERROR: Environment variable API_ENDPOINT is not a proper url")
+	}
+
+	if shouldServeUI {
+		apiEndpointUrl.Path = path.Join(apiEndpointUrl.Path, "/api")
+	}
+
+	return apiEndpointUrl
+}
+
+func UiEndpointUrl() *url.URL {
+	uiEndpointStr := os.Getenv("UI_ENDPOINT")
+
+	shouldServeUI := os.Getenv("SERVE_UI") == "1"
+	if shouldServeUI {
+		uiEndpointStr = os.Getenv("PUBLIC_ENDPOINT")
+	}
+
+	uiEndpointUrl, err := url.Parse(uiEndpointStr)
+	if err != nil {
+		log.Fatalf("ERROR: Environment variable UI_ENDPOINT is not a proper url")
+	}
+
+	return uiEndpointUrl
+}
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
index 9d9739e..a327878 100644
--- a/docker-compose.example.yml
+++ b/docker-compose.example.yml
@@ -16,7 +16,7 @@ services:
     build:
       context: "."
       args:
-        - GRAPHQL_ENDPOINT=http://localhost:8000/api/graphql
+        - API_ENDPOINT=http://localhost:8000/api/
 
     restart: always
     ports:
@@ -26,21 +26,24 @@ services:
 
     environment:
       - MYSQL_URL=photoview:photo-secret@tcp(db)/photoview
+      - API_LISTEN_IP=photoview
+      - API_LISTEN_PORT=80
       - PHOTO_CACHE=/app/cache
+      - SERVE_UI=1
+
       # Change This: The publicly exposed url for the api
       # For example if the server is available from the domain example.com,
-      # change this value to http://example.com/api
-      - API_ENDPOINT=http://localhost:80/
+      # change this value to http://example.com/
       - PUBLIC_ENDPOINT=http://localhost:8000/
 
     volumes:
-      # Change This: Link photo paths from the host machine
+      - api_cache:/app/cache
+
       # Change this to the directory where your photos are located on your server.
       # If the photos are located at `/home/user/photos`, then change this value
       # to the following: `/home/user/photos:/photos:ro`.
       # You can mount multiple paths, if your photos are spread across multiple directories.
       - ./photos_path:/photos:ro
-      - api_cache:/app/cache
 
 volumes:
   db_data:
diff --git a/ui/example.env b/ui/example.env
index e68a122..b6a6f46 100644
--- a/ui/example.env
+++ b/ui/example.env
@@ -1 +1 @@
-GRAPHQL_ENDPOINT=http://localhost:4001/graphql
\ No newline at end of file
+API_ENDPOINT=http://localhost:4001/
\ No newline at end of file
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 60f5f47..b80d16b 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -10996,6 +10996,11 @@
         }
       }
     },
+    "url-join": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+      "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
+    },
     "use": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
diff --git a/ui/package.json b/ui/package.json
index 04ac9bc..2dabd8b 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -30,7 +30,8 @@
     "semantic-ui-css": "^2.4.1",
     "semantic-ui-react": "^0.88.0",
     "styled-components": "^5.0.1",
-    "subscriptions-transport-ws": "^0.9.16"
+    "subscriptions-transport-ws": "^0.9.16",
+    "url-join": "^4.0.1"
   },
   "scripts": {
     "start": "parcel start src/index.html",
diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.js
index d8b73c6..6393ffa 100644
--- a/ui/src/apolloClient.js
+++ b/ui/src/apolloClient.js
@@ -7,17 +7,20 @@ import { setContext } from 'apollo-link-context'
 import { ApolloLink, split } from 'apollo-link'
 import { getMainDefinition } from 'apollo-utilities'
 import { MessageState } from './components/messages/Messages'
+import urlJoin from 'url-join'
+
+const GRAPHQL_ENDPOINT = urlJoin(process.env.API_ENDPOINT, '/graphql')
 
 const httpLink = new HttpLink({
-  uri: process.env.GRAPHQL_ENDPOINT,
+  uri: GRAPHQL_ENDPOINT,
   credentials: 'same-origin',
 })
 
-console.log('GRAPHQL ENDPOINT', process.env.GRAPHQL_ENDPOINT)
+console.log('API ENDPOINT', process.env.API_ENDPOINT)
 
-const apiProtocol = new URL(process.env.GRAPHQL_ENDPOINT).protocol
+const apiProtocol = new URL(process.env.API_ENDPOINT).protocol
 
-let websocketUri = new URL(process.env.GRAPHQL_ENDPOINT)
+let websocketUri = new URL(GRAPHQL_ENDPOINT)
 websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:'
 
 const wsLink = new WebSocketLink({
