Learn Docker by Dockerizing A SpringBoot, SvelteKit, MariaDB, and Keycloak App
This project is a mix of lots of technologies, that will be somehow hard to dockerize together, but it will be fun along the way since it includes volumes, networks, and the glue that holds it all together docker compose, so let’s get started.
Project Structure:
As we said earlier this project consists of Spring Boot (backend server), SvelteKit (web client), MariaDB (database), and Keycloak (Authentication provider), and the project outline should look like this:
– auth/
—- realms_backups/
– client/
—- Dockerfile
—- …more sveltekit files
– server/
—- Dockerfile
—- …more spring boot files
– docker-compose.yml
Installing Docker
I’ll demonstrate how to install docker on Gentoo Linux, other Linux distros and platforms can be found here.
Installing docker has 5 steps most of which are the same on any other Linux distro:
- Installation
sudo emerge -qav app-containers/docker app-containers/docker-cli
- Enable the docker daemon on startup
sudo rc-update add docker default
- Start the docker service
sudo rc-service docker start
- Adding your user to the docker group to be able to use docker without superuser permission
sudo gpasswd -a $(whoami) docker
- Restart your shell
this is required so that the user’s groups are updated (after adding our user to the docker group we need to do this) you can do this by restarting your working session, or by typing$SHELL
in your active terminal
- Bonus, run this to make sure that docker is working just fine on your machine
docker run hello-world
Dockerizing a simple Spring Boot App:
We’ll start by creating a Spring Boot application using the initializer with the following configs:
Config | Description |
---|---|
Language | Java 11 (it’s the GOAT version so far) |
Building System | Maven (gradle is just too easy) |
Spring Boot | Version 2.7.8 (that’s what goes with Java 11 these days) |
Spring Web | Just add it from the dependencies :) |
Packaging | Jar (if you like WAR you’re on your own) |
Now after unzipping the downloaded spring project, open it in your favorite editor, and add a Rest Controller, so we can test out this thing
// controllers/HelloController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello/{name}")
public String greet(@PathVariable String name) {
return String.format("Hello, %s", name);
}
}
Now go back to the terminal and run the spring boot app using ./mvnw spring-boot:run
, now we just do a little check using curl
to make sure that everything is in its place
curl http://localhost:8080/hello/Eloi
And if everything was in its place it should print “Hello, Eloi”.
After that everything is working just fine, we need to break everything :), just kidding, but we need to change some settings around, for starters, we need to change from the weird-looking application.properties
to the awesome superior application.yml
, since it’s 2023 and everyone thinks that YAML is cooler now, after the change, we need to change the server’s port (we’ll find out why later on in this tutorial), and now the config file should look like this
server:
port: 8081
After rerunning the application we can re-test it,
curl http://localhost:8081/hello/Eloi
Just to make sure that everything is working as expected :)
Ok now we’re all set to dockerize this simple spring app, as I mentioned in the project structure outline above, the server (Spring Boot) has a file called Dockerfile, well this file tells docker how to build and run the image, fascinating isn’t it?
# build stage
FROM alpine:latest as build
RUN apk add openjdk11-jdk openjdk11-jre openjdk11-src maven
WORKDIR /app
COPY . .
RUN mvn clean install
# run stage
FROM alpine:latest as run
RUN apk add openjdk11-jre
WORKDIR /app
COPY --from=build /app/target/*.jar ./run.jar
EXPOSE 8081
CMD ["java", "-jar", "./run.jar"]
That’s the configuration needed to run a Spring Boot app inside a docker container, but I owe you some explanation, first as you can see the file is separated into two sections, build and run, well this is useful to save disk storage, I mean imaging building three applications like this, with each container having all the build files, JDK, maven, ….
It would be a nightmare, so, here we are separating the build from the run, let’s talk about the build stage for a bit, first, we’re pulling an image called Alpine Linux using the FROM
keyword the column after the image’s name specifies the version of the image to be pulled, here we’re using the latest version of Alpine available, Alpine is a light Linux distro that is suited for small containers and virtual machines like this, after pulling the image, we see
RUN apk add openjdk11-jdk openjdk11-jre openjdk11-src maven
RUN
is used to run a shell command inside the container, apk
is the package manager used by Alpine, and no it has nothing to do with Android. Now back to docker, here we’re installing JDK, JRE, and maven, so we can compile the application into a single JAR file.
WORKDIR
is just like cd
which changes the current working directory, here we’re using /app
which is just a naming, and it can be whatever you want, but /app
is just convenient enough.
COPY
is like cp
where it copies a file or directory from the given source to the given destination, here we’re copying the whole Spring Project into the container, where we will compile it.
Now to build the project and produce a single JAR file we have to run mvn clean install
inside the container, again we’ll use the RUN
keyword before it.
After the building process is done we need to prepare the run container image, and again we’re pulling the same Alpine image, but the difference here is we’re not installing JDK, or maven, since their job was done in the build stage, now we just copy the JAR file into the run container, add some magic, and we’re ready to go.
The magic:
EXPOSE
allows a port from the container to be viewed by the docker network for the host to be able to use it, remember the port we set in application.yml
was 8081, so we expose that same port.
CMD
is what docker will run in the container after starting it, but as you can notice it’s an array of strings, which is the original command string split by a space.
For example the running command is java -jar ./run.jar
, in which it becomes ["java", "-jar", "./run.jar"]
Now it’s time for action, first, we’ll need to build the image, open your terminal and navigate to the server directory, then run
docker build -t hello-spring .
Here the -t
flag specifies the name of the container’s image after building and the .
indicates the current directory which will be used for the build.
Now get rid of the build container, by removing what’s called “dangling images”, these are images that no one depends on and can be removed without damaging any other image, and there’s nothing depends on them because we actually ran the image while building and got what we wanted from it, and it’s now time for throwing it away.
TL;DR just run
docker image prune
It should prompt you, don’t freak out, just hit yes
AND NOW FOR THE REAL ACTION, WE WILL RUN THE BUILT CONTAINER
docker run -p 8080:8081 hello-spring
You can test now I’ll explain after you test your container, so you get the satisfaction of running a docker container.
curl http://localhost:8080/hello/Eloi
The docker run command attaches the specified image to a docker container, and the -p
flag specifies the port forwarding to the host from the container, just remember this magical formula -p HOST:CONTAINER
, and the final argument is the image’s name that we want to run.
Configuring and Dockerizing Keycloak
Configuring Keycloak requires two stages, the actual realm configuration, and the docker configuration for Keycloak, let’s get started
Get a Keycloak zip archive form here, we’ll use this server to make our configurations, then export the configured realm, and use it with the docker container.
Now open your browser, and go to localhost:8080
which is the Keycloak server address, then go to Administration Console
, and log in with the credentials you specified, that is admin:SOME_PASSWORD
.
First, create a realm with any name you prefer, I’ll name mine “dori”, but that’s not the topic here, after that we’ll create a new realm.
Now, we’ll create a client called “dori-client”, with Standard Flow
, OAuth 2.0 Device Authorization Grant
, and Client authentication
enabled, then create a role called “superuser”, then create a user called “nemo”, set “nemorocks” as a password to it and assign the “superuser” role to it.
Now we’ll export the realm configuration, from the project’s directory run:
cd keycloak20.0.2/bin/
./kc.sh export --dir backups --realm dori
this will produce a directory with two files, dori-realm.json
and dori-users.json
, copy those files into our project, specifically into auth/realms_backups/
We’ll be using Keycloak’s official docker image with version 20.0.2
Now for the docker part, run this magical command to import the realm, and run the docker container.
docker run -v ./auth/realms_backups/:/tmp/backups/\
-e KEYCLOAK_ADMIN=admin\
-e KEYCLOAK_ADMIN_PASSWORD=admin\
-p 8080:8080\
quay.io/keycloak/keycloak:20.0.2\
-Dkeycloak.profile.feature.upload_scripts=enabled\
-Dkeycloak.migration.action=import\
-Dkeycloak.migration.realmName=dori\
-Dkeycloak.migration.provider=dir\
-Dkeycloak.migration.dir=/tmp/backups/\
start-dev
This might be scary at first sight, but it’s not if we break it down into parts.
First, there is the -v
flag specifies volume mounting, just like the port forwarding, but this one is for volumes, i.e. it mounts a path from the host to the container.
-v /path/in/host/:/path/in/container
, and here the host directory is ./auth/realms_backups/
since there we’ll be keeping the realm backup(s).
Then we got the -e
flag, which specifies an environment variable, in this case, we’re specifying the admin’s username and password, which are “admin”, “admin” respectively.
Then we got -p
that we know that it forwards ports to the host from the container, after that the container’s name and version that we will be running, and finally, the huge run command, which the import flags, that specify where and how to do the realm import.
Great, now back to our Spring Boot app, now we need to add some Keycloak configurations to it, should be easy right?
We’ll start with the maven dependencies
<!-- pom.xml -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>20.0.2</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.0.0</version>
</dependency>
Here we’ve added Spring Boot security and Keycloak dependencies, now off to the Keycloak Configuration class:
// conf/KeycloakAdapterConfig.java
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import({KeycloakSpringBootConfigResolver.class})
public class KeycloakAdapterConfig extends KeycloakWebSecurityConfigurerAdapter {
/* Registers the KeycloakAuthenticationProvider with the authentication manager.*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
/* Defines the session authentication strategy null means no session.*/
@Bean
@Override
protected NullAuthenticatedSessionStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf()
.disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/super-hello/")
.hasRole("superuser");
}
}
Well, this is a docker tutorial, so all you need to understand from this file is the antMatchers
these specify the path, HTTP method, and who can use it, here we have a GET
method on the route /super-hello
that only can be used by a user with the superuser
role.
AND NOW for the REST API, we’ll need to add the endpoint /super-hello
, modify the HelloController
, and add:
...
import org.springframework.security.access.prepost.PreAuthorize;
...
@PreAuthorize("hasRole('superuser')")
@GetMapping("/super-hello/{name}")
public String superGreet(@PathVariable String name) {
return String.format("Super Hello, %s", name);
}
...
Finally (not really), we need to add some Keycloak configuration to application.yml
keycloak:
realm: dori
auth-server-url: "http://localhost:8080/"
resource: dori-client
public-client: true
bearer-only: true
So what about that /super-hello
request?, if we requested it’ll give us a 401 (Unauthorized) status code, So we need a token right?
We can get a token, by making a token
request to the Keycloak server
curl -X POST http://localhost:8080/realms/dori/protocol/openid-connect/token\
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=dori-client&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=nemo&password=nemorocks'
then you should get a response like this
{
"access_token": "",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "",
"scope": "email profile"
}
As you can see we have the access token to the client dori-client
from the dori
realm, using the user nemo
, now when we use the token with the /super-hello
it’ll work.
curl http://localhost:8081/super-hello/Eloi\
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
This is so cool, right? But we’re not done yet, we need to containerize the thing right?
Now we’ll introduce docker-compose that will allow us to run more than one container at the same time (not really, but it appears to do that) with a related setup, in this case, we need a network between the Spring Boot, and the Keycloak server, finally things are getting along :)
This file will be at the root of the whole project.
# docker-compose.yml
version: "3.8"
services:
auth:
image: "quay.io/keycloak/keycloak:20.0.2"
container_name: "auth"
restart: "always"
ports:
- 9090:8080
environment:
KEYCLOAK_ADMIN: "admin"
KEYCLOAK_ADMIN_PASSWORD: "admin"
volumes:
- ./auth/realms_backups/:/tmp/backups/
command: "-Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.migration.action=import -Dkeycloak.migration.realmName=dori -Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=/tmp/backups start-dev"
networks:
- auth-backend
backend:
build: ./server
ports:
- 8080:8081
depends_on:
- auth
networks:
- auth-backend
networks:
auth-backend: {}
So…, what’s going on here?
If you look close enough you’ll notice something we’ve seen before, aside from the other configuration, the services have ports
property which will do port forwarding the same as -p
, environment
is like -e
, volumes
is like -v
when using docker run
.
command
overrides CMD
in the Dockerfile, meaning whatever we put in there will be executed when the container starts.
Now let’s go over the compose file, it’s just a YAML file that tells docker-compose
what to do, first, we have the version
which is the version of the compose file, currently, the latest version is 3.8
so we’re gonna use that, now for the services array, for starters, we have the auth
service is the Keycloak server, where we specify the wanted docker image using the image
property.
build
specifies where the docker project is, i.e. a project with a Dockerfile
at its root, and build has more interesting stuff that can be found in here
depends_on
states that the backend
service will not run until the auth
service has started.
container_name
specifies what name this image’s container will be using while it’s running, so it can be accessed from the docker network (for now that’s all that we need from the name), as you can see each one of the services has a network
property which is an array that represents the networks that the container will be connected to, in this case, auth
and backend
are connected to.
Now where to get the network?
as you can see at the end of the file we can see a networks
array that defines our networks, and here we’ve defined a network called auth-backend
that will connect the Spring Boot server to the Keycloak server, easy eh?
well, it’s not as easy as it seems, but that’s all we need for this setup, you check out more about networks here.
As I said container_name
will help with the network, but how, well now that the Keycloak service is named auth
that will be used as the server’s address.
Now we can change the Keycloak address in application.yml
to http://auth:8080/
, here we’re using port 8080 since that’s the server address inside the docker network, we can still use http://localhost:9090/
if we want to, but it’s more convenient to use the docker network. and now it’s time for action.
Run docker compose up
and it’ll build the project for the first time, and start it, but if anything changes it won’t re-build the project with the newest changes, so we need to run docker compose build
after each change and the changed image will be rebuilt, and ready for running.
Just a little test to make sure that everything is in its place.
acquire the access token first, from the Keycloak server.
curl -X POST http://localhost:9090/realms/dori/protocol/openid-connect/token\
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=dori-client&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=nemo&password=nemorocks'
this will work, but if you make /super-hello
with the token returned from the previous request, it won’t work, because the token was issued to the address localhost:9090
and the Spring Boot requests the server at auth:8080
, and Keycloak is careful who can use the token and who can’t.
so to avoid situations like this, we can easily issue the token from our Spring Boot server, we’ll create an endpoint /token
that will make a request to the Keycloak server and retrieve a token that was issued for the same address.
First, we need to add a JSON Utility Dependency, since as we’ve seen earlier the response from the Keycloak server is a JSON.
<!-- pom.xml -->
...
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20220924</version>
</dependency>
...
and now we’ll create a controller that does the token retrieving request:
// controllers/TokenController.java
import org.apache.http.client.config.RequestConfig;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import java.util.List;
import java.util.Map;
@RestController
public class TokenController {
@Value("${keycloak.auth-server-url}")
private String authServerURL;
@PostMapping("/token")
public ResponseEntity<?> login(@RequestBody Map<String, String> user) {
try {
var form = List.of(
new NameValuePairImpl("client_id", "dori-client"),
new NameValuePairImpl("client_secret", "YOUR_CLIENT_SECRET"),
new NameValuePairImpl("grant_type", "password"),
new NameValuePairImpl("username", user.get("username")),
new NameValuePairImpl("password", user.get("password"))
);
var requestConfig = RequestConfig.custom().build();
var httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
var request = new HttpPost(String.format("%s/realms/dori/protocol/openid-connect/token", authServerURL));
request.setEntity(new UrlEncodedFormEntity(form));
JSONObject json = new JSONObject(EntityUtils.toString(httpClient.execute(request).getEntity()));
return ResponseEntity.ok(Map.of("token", json.get("access_token")));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(e.toString());
}
}
}
class NameValuePairImpl implements NameValuePair {
private final String name;
private final String value;
public NameValuePairImpl(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public String getName() {
return name;
}
@Override
public String getValue() {
return value;
}
}
This controller only has one endpoint, that is /token
, so we’ll send a json with username
and password
, which will be used for logging in to the Keycloak realm.
Now we can rebuild the images, and test the requests again.
docker compose build
docker compose up
curl -X POST http://localhost:8080/token\
-H "Content-Type: application/json"\
--data '{"username": "nemo", "password": "nemorocks"}'
curl http://localhost:8080/super-hello/Eloi\
-H "Authorization: Bearer ACCESS_TOKEN"
And gladly I can finally say that this part is over.
Dockerizing MariaDB
This part is cuter than Keycloak, since we’ll just create a model, a simple controller, and modify some configuration files, that should be easy.
First, we need to configure Spring Boot with JPA, now we need JPA and MariaDB dependency.
<!-- pom.xml -->
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
...
Update your dependency tree using
mvn dependency:resolve
And update your application.yml
to use MariaDB with JPA.
server:
port: 8081
spring:
datasource:
url: "jdbc:mariadb://db/someDB?useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"
username: "root"
password: "hello"
driver-class-name: "org.mariadb.jdbc.Driver"
jpa:
generate-ddl: true
keycloak:
realm: dori
auth-server-url: "http://auth:8080/"
resource: dori-client
public-client: true
bearer-only: true
Now for the model, we’ll be using a book model with string title attribute.
// models/Book.java
import javax.persistence.*;
@Entity(name = "books")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
public void setId(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
The repo
// repos/BookRepo.java
import com.example.demo.models.Book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepo extends JpaRepository<Book, Integer> {
}
and the controller
// controllers/BookController.java
import com.example.demo.models.Book;
import com.example.demo.repos.BookRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/book")
public class BookController {
@Autowired
private BookRepo bookRepo;
@GetMapping()
public List<Book> listBooks() {
return bookRepo.findAll();
}
@PostMapping()
public void addBook(@RequestBody Book book) {
bookRepo.save(book);
}
}
now back to docker, we’ll add the MariaDB container configuration to docker-compose.yml
# docker-compose.yml
version: "3.8"
services:
auth:
image: "quay.io/keycloak/keycloak:20.0.2"
container_name: "auth"
restart: "always"
ports:
- 9090:8080
environment:
KEYCLOAK_ADMIN: "admin"
KEYCLOAK_ADMIN_PASSWORD: "admin"
volumes:
- ./auth/realms_backups/:/tmp/backups/
command: "-Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.migration.action=import -Dkeycloak.migration.realmName=dori -Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=/tmp/backups/ start-dev"
networks:
- auth-backend
db:
image: "mariadb:10.9"
container_name: "db"
restart: "always"
environment:
MARIADB_ROOT_PASSWORD: "hello"
MARIADB_DATABASE: "someDB"
ports:
- 3306
volumes:
- db-config:/etc/mysql
- db-data:/var/lib/mysql
networks:
- db-backend
backend:
build: ./server
ports:
- 8080:8081
depends_on:
- auth
- db
networks:
- auth-backend
- db-backend
networks:
auth-backend: {}
db-backend: {}
volumes:
db-config:
db-data:
Here’s a new attribute in the house volumes
which defines volumes that can be used by the containers, and MariaDB needs a configuration, and data volumes, to keep the database’s data persistently.
Now re-build and run the containers
docker compose build
docker compose up
and we can test the setup now
curl -X POST http://localhost:8080/book\
-H "Content-Type: application/json"\
--data '{"title": "The Alchemist"}'
and we can retrieve that, just to make sure
curl http://localhost:8080/book
Now we can see that everything is in its place. See told you this was easy :)
Final Round, Wrapping everything up with a little frontend SvelteKit
First, we’ll create our SvelteKit skeleton project using npm:
npm init svelte@latest client
use these configs:
✔ Which Svelte app template? › Skeleton project
✔ Add type checking with TypeScript? › Yes, using TypeScript syntax
✔ Add ESLint for code linting? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Playwright for browser testing? … No / Yes
✔ Add Vitest for unit testing? … No / Yes
then install the project’s dependencies
cd client
npm install
add some stuff to src/routes/+page.svelte
to make it interactive with the backend
<!-- src/routes/+page.svelte -->
<script lang="ts">
import {onMount} from "svelte"
let title: string;
let books: {title: string}[];
async function createBook() {
await fetch("http://localhost:8080/book", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({title: title}),
})
.then(resp => {
if (resp.ok) {
updateBooksList();
}
})
.catch(err => {
console.error(err);
})
}
async function updateBooksList() {
books = await fetch("http://localhost:8080/book", {
method: "GET",
mode: "cors",
})
.then(resp => resp.json())
.then(fetchedBooks => fetchedBooks) as {title: string}[];
}
onMount(async () => {
await updateBooksList();
})
</script>
<div>
<input bind:value={title} placeholder="Book Title" />
<button on:click={createBook}>Add Book</button>
<br/>
{#if books}
<title>Book:</title>
<ul id="books">
{#each books as book}
<li>
{book.title}
</li>
{/each}
</ul>
{/if}
</div>
Now for the docker part, first install @sveltejs/adapter-node
to make it a standalone server, to save the effort of making a server, and dealing with the routes, but keep in mind that the node adapter uses port 3000.
Then update svelte.config.js
// import adapter from "@sveltejs/adapter-auto";
import adapter from "@sveltejs/adapter-node";
add the client’s Dockerfile
FROM node:16-alpine as build
WORKDIR /app
COPY . .
RUN npm i
RUN npm run build
FROM node:16-alpine as run
WORKDIR /app
COPY --from=build /app/package*.json ./
COPY --from=build /app/build ./
EXPOSE 3000
CMD ["node", "./index.js"]
And now, for the final version of docker-compose.yml
# docker-compose.yml
version: "3.8"
services:
auth:
image: "quay.io/keycloak/keycloak:20.0.2"
container_name: "auth"
restart: "always"
ports:
- 9090:8080
environment:
KEYCLOAK_ADMIN: "admin"
KEYCLOAK_ADMIN_PASSWORD: "admin"
volumes:
- ./auth/realms_backups/:/tmp/backups/
command: "-Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.migration.action=import -Dkeycloak.migration.realmName=dori -Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=/tmp/backups/ start-dev"
networks:
- auth-backend
db:
image: "mariadb:10.9"
container_name: "db"
restart: "always"
environment:
MARIADB_ROOT_PASSWORD: "hello"
MARIADB_DATABASE: "someDB"
ports:
- 3306
volumes:
- db-config:/etc/mysql
- db-data:/var/lib/mysql
networks:
- db-backend
backend:
build: ./server
ports:
- 8080:8081
depends_on:
- auth
- db
networks:
- auth-backend
- db-backend
frontend:
build: ./client
depends_on:
- backend
ports:
- 8081:3000
networks:
auth-backend: {}
db-backend: {}
volumes:
db-config:
db-data:
As usual, build and run, and you should see some results.
And now we’re done.