Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin/
obj/
.vs/
52 changes: 9 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,13 @@
# Docker Image
# Learn DevOps

Docker images are created using dockerfiles. A sample dockerfile, from which
you can create docker images, is given at the root of the repository. This
sample dockerfile will be used in the demo section.
This is a hands-on learning repository where we explore and experiment with
DevOps tools, technologies, and best practices.

## Create image with dockerfile
Each topic is organized in its own directory with dedicated documentation and
examples.

To build a docker image, run `docker build . ` at the directory your
dockerfile is in. This searches for a dockerfile in the run directory and
creates a docker image from found dockerfile. You can give name to docker image
with `-t {image-name}`, `docker build -t {image-name} . `
## What We've Learned So Far

## Run image in container

`docker run`, creates an instance of docker image and runs it in a container.
Adding args at the end of the run command will run those arguments, thus you
can pass commands as arguments.

`docker run {image-name} args`

## Persisting Data

To bind a host directory to container, use `docker run` with
`-v {absolutepath}:{containerpath}`.

`docker run -v {absolutepath}:{containerpath} {image-name} args`

Changes made in this folder by the container persists, you can observe these
changes in host folder. To get the output of the container in host folder, set
the output as the container folder, which is bound to host folder.

`docker run --rm -v ${PWD}/host-output:/container-output args {-output} /container-output`

## Reaching Localhost from Container

To reach your machine's localhost from the container, when giving the url use
`host.docker.internal` instead of localhost.

`http://host.docker.internal:{port}`.

## Demo

Dockerfile at the root of this repository wraps
[web-ping](https://github.com/SeriaWei/Ping). You can create a docker
image from this dockerfile and use this cli in your container by passing
`web-ping Web.Ping --host https://github.com/` command as argument.
- [Docker](docker/README.md): Docker images, Dockerfiles, and container basics
- [Keycloak](keycloak/README.md): Identity management, JWT authentication,
Docker Compose setup
47 changes: 47 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Docker Image

Docker images are created using dockerfiles. A sample dockerfile, from which
you can create docker images, is given at the root of the repository. This
sample dockerfile will be used in the demo section.

## Create image with dockerfile

To build a docker image, run `docker build . ` at the directory your
dockerfile is in. This searches for a dockerfile in the run directory and
creates a docker image from found dockerfile. You can give name to docker image
with `-t {image-name}`, `docker build -t {image-name} . `

## Run image in container

`docker run`, creates an instance of docker image and runs it in a container.
Adding args at the end of the run command will run those arguments, thus you
can pass commands as arguments.

`docker run {image-name} args`

## Persisting Data

To bind a host directory to container, use `docker run` with
`-v {absolutepath}:{containerpath}`.

`docker run -v {absolutepath}:{containerpath} {image-name} args`

Changes made in this folder by the container persists, you can observe these
changes in host folder. To get the output of the container in host folder, set
the output as the container folder, which is bound to host folder.

`docker run --rm -v ${PWD}/host-output:/container-output args {-output} /container-output`

## Reaching Localhost from Container

To reach your machine's localhost from the container, when giving the url use
`host.docker.internal` instead of localhost.

`http://host.docker.internal:{port}`.

## Demo

Dockerfile at the root of this repository wraps
[web-ping](https://github.com/SeriaWei/Ping). You can create a docker
image from this dockerfile and use this cli in your container by passing
`web-ping Web.Ping --host https://github.com/` command as argument.
8 changes: 4 additions & 4 deletions dockerfile → docker/dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Parent image
FROM mcr.microsoft.com/dotnet/sdk:6.0
RUN dotnet tool install --global Web.Ping --version 0.0.4
# Parent image
FROM mcr.microsoft.com/dotnet/sdk:6.0

RUN dotnet tool install --global Web.Ping --version 0.0.4
ENV PATH="/root/.dotnet/tools:${PATH}"
169 changes: 169 additions & 0 deletions keycloak/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Keycloak

This documentation covers using Keycloak with Docker in this repo.

## Configuration

Keycloak can be configured in four ways:

1. Command-line parameters
2. Environment variables
3. Options in `conf/keycloak.conf` (or a user-provided config file)
4. Sensitive options in a Java KeyStore

We use the config file approach. Options follow the format
`<key-with-dashes>=<value>`.

- Default config path: `conf/keycloak.conf`
- Environment placeholders: `${ENV_VAR}` with optional fallback
`${ENV_VAR:default}`

All available options: [All Configs]

Note: Some realm settings are restricted at runtime; enabling flags (e.g.,
`spi-admin-allowed-system-variables`) should be used cautiously.

### Database

You can configure the database via the config file, environment variables, or
CLI flags. Precedence: CLI > environment > config file.

Using `keycloak.conf`:

```
db-url-host=mykeycloakdb
```

### Quarkus framework

For gaps in Keycloak options, you can fall back to raw Quarkus properties:
[Quarkus Properties]

## Import realms

Keycloak creates a `master` realm by default; avoid using it for applications.
You can create realms via the UI or Admin API, or import a prebuilt JSON by
copying it into the image under `/opt/keycloak/data/import/` and starting with
realm import enabled.

## Modes

Keycloak runs in development (default) and production modes. Some features
differ: [Dev Mode]

### Production mode

Production requires additional setup:

- HTTP disabled; HTTPS (TLS) required
- Hostname configuration required
- HTTPS/TLS configuration required

## Optimizations

For faster startup in containers, use the recommended flow:

1. Build once normally
2. Start with `--optimized` to reuse the build

If runtime build config conflicts with a pre-build, the pre-built assets take
precedence.

## UI

Access the Admin Console at `{base-url}/admin` to manage realms, users, and
settings.

## API

Manage Keycloak via the Admin REST API under `{base-url}/admin`.
Example: `POST /admin/realms/{realm}/logout-all`
API reference: [API Reference]

OpenID Connect discovery: [OIDC Discovery]

## Realms

Realms isolate users and configuration. The `master` realm exists by default and
should be used only for administering Keycloak.

## Token

Use Protocol Mappers to add claims to tokens. For example, adding an audience
claim uses the `oidc-audience-mapper`. See `keycloak/realm-config.json` in this
repo for basic examples.

## Login and Redirect

To start the login flow, send a GET request to:

```
GET http://localhost:8080/realms/<realm-name>/protocol/openid-connect/auth
```

Query parameters:
- `client_id`: The client ID (e.g. weather-api)
- `response_type`: `code` (for Authorization Code Flow)
- `scope`: `openid` (and any additional scopes)
- `redirect_uri`: Where the user will be redirected after login
(e.g. http://localhost)
- `prompt`: (optional) Controls whether the login screen is shown. Use
`prompt=none` to attempt silent authentication (no UI); if the user is not
already logged in, an error is returned instead of showing the login page.
This is useful for checking session status or implementing silent SSO.

Request:
```url
http://localhost:8080/realms/test-realm/protocol/openid-connect/auth
?client_id=weather-api
&response_type=code
&scope=openid
&redirect_uri=http://localhost
&prompt=none
```

After successful login, Keycloak redirects to:
```
http://localhost/?code=AUTH_CODE&session_state=...&iss=...
```

To exchange the `code` for an access token, send a POST request to:
```
POST http://localhost:8080/realms/<realm-name>/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

client_id=weather-api
&grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=http://localhost
```

#### Redirect URI Settings

For the redirect to work, the client must have:
- `redirectUris`: Allowed redirect URIs (e.g. ["http://localhost/*"])
- `webOrigins`: Allowed CORS origins (e.g. ["http://localhost"])
- `standardFlowEnabled`: true (required for Authorization Code Flow)

> [Info]
>
> If you want to obtain tokens directly from a frontend (SPA/JS) app, set
> `publicClient: true` and do not send `client_secret` in the token request. For
> confidential clients (`publicClient: false`), using the secret in the frontend
> is insecure and required by Keycloak.

These settings must be present in both JSON imports and in the Keycloak UI
client configuration.

## Docker

For production, size memory appropriately. Guidance: [Sizing Guide]

## References

[All Configs]: https://www.keycloak.org/server/all-config?f=build
[Quarkus Properties]: https://www.keycloak.org/server/configuration#_format_for_raw_quarkus_properties
[Dev Mode]: https://www.keycloak.org/server/configuration#_starting_keycloak_in_development_mode
[API Reference]: https://www.keycloak.org/docs-api/latest/rest-api/index.html
[OIDC Discovery]: http://localhost:8080/realms/master/.well-known/openid-configuration
[Sizing Guide]: https://www.keycloak.org/high-availability/single-cluster/concepts-memory-and-cpu-sizing#single-cluster-single-site-calculation
97 changes: 97 additions & 0 deletions keycloak/Ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Keycloak Flow Demo</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
#output { background: #f4f4f4; padding: 10px; border-radius: 5px; }
button { padding: 10px 20px; cursor: pointer; }
</style>
</head>
<body>
<h1>Weather App</h1>
<button onclick="loadWeather()">Load Weather</button>
<pre id="output">Click "Load Weather" to start</pre>

<script>
const keycloakUrl = 'http://localhost:8080/realms/test-realm';
const clientId = 'weather-api';
const backendUrl = 'http://localhost:5000';
const redirectUri = window.location.origin;

window.addEventListener('DOMContentLoaded', async () => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');

// Remove code from URL
if (code) {
window.history.replaceState({}, document.title, window.location.pathname);
await loginByCode(code);
}
});

async function loginByCode(code) {
try {
const res = await fetch(`${backendUrl}/login-by-code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, redirectUri })
});

if (!res.ok) {
throw new Error("login failed");
}

const data = await res.json();

localStorage.setItem('accessToken', data.accessToken);

await loadWeather();
} catch (err) {
document.getElementById('output').textContent =
'Login error: ' + err.message;
}
}

async function loadWeather() {
const accessToken = localStorage.getItem('accessToken');
if (!accessToken) {
redirectToLogin();
return;
}

try {
const res = await fetch(`${backendUrl}/weather`, {
headers: {
Authorization: 'Bearer ' + accessToken
}
});

if (res.status === 401) {
redirectToLogin();
return;
}

const data = await res.json();
document.getElementById('output').textContent =
`Temperature: ${data.temperature}°C\nSky: ${data.sky}`;
} catch (err) {
document.getElementById('output').textContent =
'Weather error: ' + err.message;
}
}

function redirectToLogin() {
const authUrl =
`${keycloakUrl}/protocol/openid-connect/auth` +
`?client_id=${clientId}` +
`&response_type=code` +
`&scope=openid` +
`&redirect_uri=${encodeURIComponent(redirectUri)}`;

window.location.href = authUrl;
}
</script>
</body>
</html>
Loading