21 Commits

Author SHA1 Message Date
36356093ff Add video examples 2025-06-20 20:32:06 -04:00
03e9317041 Rename folder for clarity 2025-06-20 19:59:32 -04:00
b6c684cdd1 Simplify startup 2025-06-20 13:30:10 -04:00
8e2cd7f9a1 Cleanup 2025-06-20 12:48:25 -04:00
fa25dde20f Add linkedin link 2025-06-20 12:46:12 -04:00
1696808c00 Fix devcontainer setup 2025-06-20 10:47:20 -04:00
7aa80d9800 Cleanup backend dockerfile 2025-06-19 21:14:49 -04:00
761ea8fef8 Simplify slides 2025-06-19 13:05:44 -04:00
94a71d6d77 Clean up compose 2025-06-19 12:26:09 -04:00
66fc64dcac Update slides and cleanup examples 2025-06-19 12:05:32 -04:00
3f0d1d77ca Update links to repo 2025-06-13 20:32:13 -04:00
7c6e8a88a3 Add demo on devcontainer 2025-06-13 20:12:08 -04:00
302cc66d55 Restructure and clarify 2025-06-13 16:13:57 -04:00
d4fe8c4575 Add qrcode and general updates 2025-06-11 19:33:19 -04:00
fe46579aaa Restructure folder 2025-06-11 19:03:54 -04:00
87e8758e40 Reduce number of seeded users for faster start 2025-06-11 17:47:51 -04:00
8327d34708 Add some more docs and streamline entrypoint 2025-06-11 17:08:21 -04:00
78f32b4384 Add user display page 2025-06-11 16:28:28 -04:00
1a58bc3220 Add fullstack example 2025-06-11 16:15:52 -04:00
f98e05b677 Add fullstack example 2025-06-11 15:23:27 -04:00
0313358d16 Remove dups 2025-06-11 15:10:14 -04:00
84 changed files with 11414 additions and 98 deletions

View File

@ -1,11 +1,21 @@
# Welcome to [Slidev](https://github.com/slidevjs/slidev)!
# Demystifying Docker
This repo is a work in progress documenting my upcoming presentation for Scenic City Summit 2025. This repo is currently split into 2 parts.
To start the slide show:
## Slide Deck presentaiton
The slides folder contains my slide deck for the event. To view the presentation:
- `pnpm install`
- `pnpm dev`
- visit <http://localhost:3030>
```shell
cd slides
yarn
yarn dev
```
Once everything is done it should open a browser window to http://localhost:3030. I am using [slidev](https://sli.dev) for the slide deck. All of the "slides" are in a single markdown file in the slides directory.
## Code examples
The examples folder contains several Docker examples.
`examples/fullstack` is a basic example containing a backend NodeJS application with a React frontend as well as a Postgres Database. The project is set up so that you can do all development in dev containers. The `node_modules` folder for the backend is isolated from your host system and only exists inside the running container. For this reason, you will need to use a devcontainer in order to be able to get things like Intellisense and typings.
Edit the [slides.md](./slides.md) to see the changes.
Learn more about Slidev at the [documentation](https://sli.dev/).

Binary file not shown.

View File

@ -0,0 +1,18 @@
{
"name": "Fullstack DevContainer",
"dockerComposeFile": ["../compose.yml", "../compose.override.yml"],
"service": "backend",
"workspaceFolder": "/app",
"shutdownAction": "stopCompose",
"forwardPorts": [80, 8080],
"postCreateCommand": "/entrypoint.sh",
"customizations": {
"vscode": {
"extensions": [
"jripouteau.adonis-vscode-extension",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
}
}

View File

@ -0,0 +1,2 @@
POSTGRES_PASSWORD=
DB_CONNECTION_STRING=

View File

@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

3
examples/devcontainers/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
**/node_modules
**/dist
.env

View File

@ -0,0 +1,49 @@
# Overview
This is a practical example of a "basic" fullstack application. This application has been fully containerized to be run locally in development mode as well as for deployment to production. It is composed of the following services:
- frontend - Basic React app bootstrapped with `yarn create vite`
- backend - AdonisJS API Backend [https://adonisjs.com]()
- database - Postgres database
- reverse_proxy - Traefik ingress controller handling reverse proxy for the frontend and backend applications.
## Getting Started
### Devcontainer
You can run this project via dev containers. First you will need to ensure that you have the [VSCode dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
Once you have the extension installed, there are a couple ways to get started. From this directory run `code .` to open a new VSCode window targeting this directory. After a few seconds you should get a notification in the bottom right asking if you want to reopen the workspace in a container. This will open a new VScode window and will start the process of building and running all of the containers.
<video src="./open-devcontainers-option1.m4v" controls></video>
Another method is to open the `Command Palette` (CMD+Shift+P) and search for `Dev Containers: Open Folder in Dev Container`. Select this folder and VScode will open a new window and kick off the process. The very first time you do this it will take several minutes to build the images and start the containers.
<video src="./open-devcontainers-option2.m4v" controls></video>
Alternatively, you can also just use the compose files to start a stack and connect your VSCode instance to the backend container.
To do that, clone the repo and `cd` into this directory.
```shell
cd examples/devcontainers
docker compose up
```
The first time you run `docker compose up` it may take a few minutes as Docker will need to build images for the frontend and backend. Also there are some database migrations and seed data that need to happen. Those are handled by `backend/entrypoint.sh`. This is handled for you automatically since it is baked into the image. Once everything is up you should be able to access the frontend at [http://app.docker.localhost:8888]() and the backend at [http://app.docker.localhost:8888/api]()
![](./homepage.png)
## Developing
When running the `compose` stack, the backend/frontend node_modules are isolated from your host system. This is common in dev environment setups. In this case it is advised to use a Dev container. The following video demonstrates starting a dev container by using the VSCode plugin.
<video src="../../assets/open-dev-container.m4v" controls></video>
### Proxying and routes
You will notice that the backend is listening on `http://0.0.0.0:3333/` but the frontend is making requests to `/api/`. This is designed to mimic how you would deploy something like this in production where the backend would be behind a reverse proxy. Traefik has some really nice middleware that allows us to easily tell it to route requests destined for `/api` to `/`. The bit that handles that are these labels:
```compose
labels:
- "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api"
- "traefik.http.routers.backend.rule=Host(`app.docker.localhost`) && PathPrefix(`/api`)"
- "traefik.http.routers.backend.middlewares=strip-api-prefix@docker"
```
We are defining a middleware named `strip-api-prefix` using the built in `strippreffix` Traefik middleware. We are then telling it to remove `/api` from any requests it handles. We then attach that to our backend router.

View File

@ -0,0 +1,19 @@
# Adonis default .gitignore ignores
node_modules
build
coverage
.vscode
.DS_STORE
.env
tmp
# Additional .gitignore ignores (any custom file you wish)
.idea
# Additional good to have ignores for dockerignore
Dockerfile*
docker-compose*
.dockerignore
*.md
.git
.gitignore

View File

@ -0,0 +1,22 @@
# http://editorconfig.org
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.json]
insert_final_newline = unset
[**.min.js]
indent_style = unset
insert_final_newline = unset
[MakeFile]
indent_style = space
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,12 @@
TZ=UTC
PORT=3333
HOST=0.0.0.0
LOG_LEVEL=info
APP_KEY=
NODE_ENV=development
DB_HOST=db
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_DATABASE=postgres
SESSION_DRIVER=cookie

View File

@ -0,0 +1,25 @@
# Dependencies and AdonisJS build
node_modules
build
tmp
# Secrets
.env
.env.local
.env.production.local
.env.development.local
# Frontend assets compiled code
public/assets
# Build tools specific
npm-debug.log
yarn-error.log
# Editors specific
.fleet
.idea
.vscode
# Platform specific
.DS_Store

View File

@ -0,0 +1,42 @@
FROM node:lts-alpine3.22 AS base
HEALTHCHECK --interval=5s \
--timeout=10s \
--start-period=5s \
--retries=5 \
CMD sh -c 'wget --no-verbose --tries=1 --spider http://127.0.0.1:3333 || exit 1'
# All deps stage
FROM base AS deps
WORKDIR /app/backend
ADD package.json package-lock.json ./
RUN npm ci
FROM deps AS develop
WORKDIR /app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 3333
ENTRYPOINT [ "/entrypoint.sh" ]
# Production only deps stage
FROM base AS production-deps
WORKDIR /app/backend
ADD package.json package-lock.json ./
RUN npm ci --omit=dev
# Build stage
FROM base AS build
WORKDIR /app/backend
COPY --from=deps /app/backend/node_modules /app/backend/node_modules
ADD . .
RUN node ace build
# Production stage
FROM base
ENV NODE_ENV=production
WORKDIR /app
COPY --from=production-deps /app/backend/node_modules /app/backend/node_modules
COPY --from=build /app/backend/build /app
EXPOSE 8080
CMD ["node", "./bin/server.js"]

View File

@ -0,0 +1,27 @@
/*
|--------------------------------------------------------------------------
| JavaScript entrypoint for running ace commands
|--------------------------------------------------------------------------
|
| DO NOT MODIFY THIS FILE AS IT WILL BE OVERRIDDEN DURING THE BUILD
| PROCESS.
|
| See docs.adonisjs.com/guides/typescript-build-process#creating-production-build
|
| Since, we cannot run TypeScript source code using "node" binary, we need
| a JavaScript entrypoint to run ace commands.
|
| This file registers the "ts-node/esm" hook with the Node.js module system
| and then imports the "bin/console.ts" file.
|
*/
/**
* Register hook to process TypeScript files using ts-node
*/
import 'ts-node-maintained/register/esm'
/**
* Import ace console entrypoint
*/
await import('./bin/console.js')

View File

@ -0,0 +1,87 @@
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
/*
|--------------------------------------------------------------------------
| Experimental flags
|--------------------------------------------------------------------------
|
| The following features will be enabled by default in the next major release
| of AdonisJS. You can opt into them today to avoid any breaking changes
| during upgrade.
|
*/
experimental: {
mergeMultipartFieldsAndFiles: true,
shutdownInReverseOrder: true,
},
/*
|--------------------------------------------------------------------------
| Commands
|--------------------------------------------------------------------------
|
| List of ace commands to register from packages. The application commands
| will be scanned automatically from the "./commands" directory.
|
*/
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands')],
/*
|--------------------------------------------------------------------------
| Service providers
|--------------------------------------------------------------------------
|
| List of service providers to import and register when booting the
| application
|
*/
providers: [
() => import('@adonisjs/core/providers/app_provider'),
() => import('@adonisjs/core/providers/hash_provider'),
{
file: () => import('@adonisjs/core/providers/repl_provider'),
environment: ['repl', 'test'],
},
() => import('@adonisjs/core/providers/vinejs_provider'),
() => import('@adonisjs/cors/cors_provider'),
() => import('@adonisjs/lucid/database_provider'),
() => import('@adonisjs/session/session_provider'),
() => import('@adonisjs/auth/auth_provider')
],
/*
|--------------------------------------------------------------------------
| Preloads
|--------------------------------------------------------------------------
|
| List of modules to import before starting the application.
|
*/
preloads: [() => import('#start/routes'), () => import('#start/kernel')],
/*
|--------------------------------------------------------------------------
| Tests
|--------------------------------------------------------------------------
|
| List of test suites to organize tests by their type. Feel free to remove
| and add additional suites.
|
*/
tests: {
suites: [
{
files: ['tests/unit/**/*.spec(.ts|.js)'],
name: 'unit',
timeout: 2000,
},
{
files: ['tests/functional/**/*.spec(.ts|.js)'],
name: 'functional',
timeout: 30000,
},
],
forceExit: false,
},
})

View File

@ -0,0 +1,8 @@
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
export default class UsersController {
public async index({ request }: HttpContext) {
const page = request.input('page', 1)
return User.query().paginate(page)
}
}

View File

@ -0,0 +1,28 @@
import app from '@adonisjs/core/services/app'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
/**
* In debug mode, the exception handler will display verbose errors
* with pretty printed stack traces.
*/
protected debug = !app.inProduction
/**
* The method is used for handling errors and returning
* response to the client
*/
async handle(error: unknown, ctx: HttpContext) {
return super.handle(error, ctx)
}
/**
* The method is used to report error to the logging service or
* the third party error monitoring service.
*
* @note You should not attempt to send a response from this method.
*/
async report(error: unknown, ctx: HttpContext) {
return super.report(error, ctx)
}
}

View File

@ -0,0 +1,25 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import type { Authenticators } from '@adonisjs/auth/types'
/**
* Auth middleware is used authenticate HTTP requests and deny
* access to unauthenticated users.
*/
export default class AuthMiddleware {
/**
* The URL to redirect to, when authentication fails
*/
redirectTo = '/login'
async handle(
ctx: HttpContext,
next: NextFn,
options: {
guards?: (keyof Authenticators)[]
} = {}
) {
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
return next()
}
}

View File

@ -0,0 +1,19 @@
import { Logger } from '@adonisjs/core/logger'
import { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
/**
* The container bindings middleware binds classes to their request
* specific value using the container resolver.
*
* - We bind "HttpContext" class to the "ctx" object
* - And bind "Logger" class to the "ctx.logger" object
*/
export default class ContainerBindingsMiddleware {
handle(ctx: HttpContext, next: NextFn) {
ctx.containerResolver.bindValue(HttpContext, ctx)
ctx.containerResolver.bindValue(Logger, ctx.logger)
return next()
}
}

View File

@ -0,0 +1,16 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
/**
* Updating the "Accept" header to always accept "application/json" response
* from the server. This will force the internals of the framework like
* validator errors or auth errors to return a JSON response.
*/
export default class ForceJsonResponseMiddleware {
async handle({ request }: HttpContext, next: NextFn) {
const headers = request.headers()
headers.accept = 'application/json'
return next()
}
}

View File

@ -0,0 +1,31 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import type { Authenticators } from '@adonisjs/auth/types'
/**
* Guest middleware is used to deny access to routes that should
* be accessed by unauthenticated users.
*
* For example, the login page should not be accessible if the user
* is already logged-in
*/
export default class GuestMiddleware {
/**
* The URL to redirect to when user is logged-in
*/
redirectTo = '/'
async handle(
ctx: HttpContext,
next: NextFn,
options: { guards?: (keyof Authenticators)[] } = {}
) {
for (let guard of options.guards || [ctx.auth.defaultGuard]) {
if (await ctx.auth.use(guard).check()) {
return ctx.response.redirect(this.redirectTo, true)
}
}
return next()
}
}

View File

@ -0,0 +1,19 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
/**
* Silent auth middleware can be used as a global middleware to silent check
* if the user is logged-in or not.
*
* The request continues as usual, even when the user is not logged-in.
*/
export default class SilentAuthMiddleware {
async handle(
ctx: HttpContext,
next: NextFn,
) {
await ctx.auth.check()
return next()
}
}

View File

@ -0,0 +1,30 @@
import { DateTime } from 'luxon'
import hash from '@adonisjs/core/services/hash'
import { compose } from '@adonisjs/core/helpers'
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
uids: ['email'],
passwordColumnName: 'password',
})
export default class User extends compose(BaseModel, AuthFinder) {
@column({ isPrimary: true })
declare id: number
@column()
declare fullName: string | null
@column()
declare email: string
@column({ serializeAs: null })
declare password: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime | null
}

View File

@ -0,0 +1,47 @@
/*
|--------------------------------------------------------------------------
| Ace entry point
|--------------------------------------------------------------------------
|
| The "console.ts" file is the entrypoint for booting the AdonisJS
| command-line framework and executing commands.
|
| Commands do not boot the application, unless the currently running command
| has "options.startApp" flag set to true.
|
*/
import 'reflect-metadata'
import { Ignitor, prettyPrintError } from '@adonisjs/core'
/**
* URL to the application root. AdonisJS need it to resolve
* paths to file and directories for scaffolding commands
*/
const APP_ROOT = new URL('../', import.meta.url)
/**
* The importer is used to import files in context of the
* application.
*/
const IMPORTER = (filePath: string) => {
if (filePath.startsWith('./') || filePath.startsWith('../')) {
return import(new URL(filePath, APP_ROOT).href)
}
return import(filePath)
}
new Ignitor(APP_ROOT, { importer: IMPORTER })
.tap((app) => {
app.booting(async () => {
await import('#start/env')
})
app.listen('SIGTERM', () => app.terminate())
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
})
.ace()
.handle(process.argv.splice(2))
.catch((error) => {
process.exitCode = 1
prettyPrintError(error)
})

View File

@ -0,0 +1,45 @@
/*
|--------------------------------------------------------------------------
| HTTP server entrypoint
|--------------------------------------------------------------------------
|
| The "server.ts" file is the entrypoint for starting the AdonisJS HTTP
| server. Either you can run this file directly or use the "serve"
| command to run this file and monitor file changes
|
*/
import 'reflect-metadata'
import { Ignitor, prettyPrintError } from '@adonisjs/core'
/**
* URL to the application root. AdonisJS need it to resolve
* paths to file and directories for scaffolding commands
*/
const APP_ROOT = new URL('../', import.meta.url)
/**
* The importer is used to import files in context of the
* application.
*/
const IMPORTER = (filePath: string) => {
if (filePath.startsWith('./') || filePath.startsWith('../')) {
return import(new URL(filePath, APP_ROOT).href)
}
return import(filePath)
}
new Ignitor(APP_ROOT, { importer: IMPORTER })
.tap((app) => {
app.booting(async () => {
await import('#start/env')
})
app.listen('SIGTERM', () => app.terminate())
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
})
.httpServer()
.start()
.catch((error) => {
process.exitCode = 1
prettyPrintError(error)
})

View File

@ -0,0 +1,62 @@
/*
|--------------------------------------------------------------------------
| Test runner entrypoint
|--------------------------------------------------------------------------
|
| The "test.ts" file is the entrypoint for running tests using Japa.
|
| Either you can run this file directly or use the "test"
| command to run this file and monitor file changes.
|
*/
process.env.NODE_ENV = 'test'
import 'reflect-metadata'
import { Ignitor, prettyPrintError } from '@adonisjs/core'
import { configure, processCLIArgs, run } from '@japa/runner'
/**
* URL to the application root. AdonisJS need it to resolve
* paths to file and directories for scaffolding commands
*/
const APP_ROOT = new URL('../', import.meta.url)
/**
* The importer is used to import files in context of the
* application.
*/
const IMPORTER = (filePath: string) => {
if (filePath.startsWith('./') || filePath.startsWith('../')) {
return import(new URL(filePath, APP_ROOT).href)
}
return import(filePath)
}
new Ignitor(APP_ROOT, { importer: IMPORTER })
.tap((app) => {
app.booting(async () => {
await import('#start/env')
})
app.listen('SIGTERM', () => app.terminate())
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
})
.testRunner()
.configure(async (app) => {
const { runnerHooks, ...config } = await import('../tests/bootstrap.js')
processCLIArgs(process.argv.splice(2))
configure({
...app.rcFile.tests,
...config,
...{
setup: runnerHooks.setup,
teardown: runnerHooks.teardown.concat([() => app.terminate()]),
},
})
})
.run(() => run())
.catch((error) => {
process.exitCode = 1
prettyPrintError(error)
})

View File

@ -0,0 +1,40 @@
import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { Secret } from '@adonisjs/core/helpers'
import { defineConfig } from '@adonisjs/core/http'
/**
* The app key is used for encrypting cookies, generating signed URLs,
* and by the "encryption" module.
*
* The encryption module will fail to decrypt data if the key is lost or
* changed. Therefore it is recommended to keep the app key secure.
*/
export const appKey = new Secret(env.get('APP_KEY'))
/**
* The configuration settings used by the HTTP server
*/
export const http = defineConfig({
generateRequestId: true,
allowMethodSpoofing: false,
/**
* Enabling async local storage will let you access HTTP context
* from anywhere inside your application.
*/
useAsyncLocalStorage: false,
/**
* Manage cookies configuration. The settings for the session id cookie are
* defined inside the "config/session.ts" file.
*/
cookie: {
domain: '',
path: '/',
maxAge: '2h',
httpOnly: true,
secure: app.inProduction,
sameSite: 'lax',
},
})

View File

@ -0,0 +1,28 @@
import { defineConfig } from '@adonisjs/auth'
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session'
import type { InferAuthenticators, InferAuthEvents, Authenticators } from '@adonisjs/auth/types'
const authConfig = defineConfig({
default: 'web',
guards: {
web: sessionGuard({
useRememberMeTokens: false,
provider: sessionUserProvider({
model: () => import('#models/user')
}),
}),
},
})
export default authConfig
/**
* Inferring types from the configured auth
* guards.
*/
declare module '@adonisjs/auth/types' {
export interface Authenticators extends InferAuthenticators<typeof authConfig> {}
}
declare module '@adonisjs/core/types' {
interface EventsList extends InferAuthEvents<Authenticators> {}
}

View File

@ -0,0 +1,55 @@
import { defineConfig } from '@adonisjs/core/bodyparser'
const bodyParserConfig = defineConfig({
/**
* The bodyparser middleware will parse the request body
* for the following HTTP methods.
*/
allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
/**
* Config for the "application/x-www-form-urlencoded"
* content-type parser
*/
form: {
convertEmptyStringsToNull: true,
types: ['application/x-www-form-urlencoded'],
},
/**
* Config for the JSON parser
*/
json: {
convertEmptyStringsToNull: true,
types: [
'application/json',
'application/json-patch+json',
'application/vnd.api+json',
'application/csp-report',
],
},
/**
* Config for the "multipart/form-data" content-type parser.
* File uploads are handled by the multipart parser.
*/
multipart: {
/**
* Enabling auto process allows bodyparser middleware to
* move all uploaded files inside the tmp folder of your
* operating system
*/
autoProcess: true,
convertEmptyStringsToNull: true,
processManually: [],
/**
* Maximum limit of data to parse including all files
* and fields
*/
limit: '20mb',
types: ['multipart/form-data'],
},
})
export default bodyParserConfig

View File

@ -0,0 +1,19 @@
import { defineConfig } from '@adonisjs/cors'
/**
* Configuration options to tweak the CORS policy. The following
* options are documented on the official documentation website.
*
* https://docs.adonisjs.com/guides/security/cors
*/
const corsConfig = defineConfig({
enabled: true,
origin: true,
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
headers: true,
exposeHeaders: [],
credentials: true,
maxAge: 90,
})
export default corsConfig

View File

@ -0,0 +1,24 @@
import env from '#start/env'
import { defineConfig } from '@adonisjs/lucid'
const dbConfig = defineConfig({
connection: 'postgres',
connections: {
postgres: {
client: 'pg',
connection: {
host: env.get('DB_HOST'),
port: env.get('DB_PORT'),
user: env.get('DB_USER'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
migrations: {
naturalSort: true,
paths: ['database/migrations'],
},
},
},
})
export default dbConfig

View File

@ -0,0 +1,24 @@
import { defineConfig, drivers } from '@adonisjs/core/hash'
const hashConfig = defineConfig({
default: 'scrypt',
list: {
scrypt: drivers.scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 33554432,
}),
},
})
export default hashConfig
/**
* Inferring types for the list of hashers you have configured
* in your application.
*/
declare module '@adonisjs/core/types' {
export interface HashersList extends InferHashers<typeof hashConfig> {}
}

View File

@ -0,0 +1,35 @@
import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { defineConfig, targets } from '@adonisjs/core/logger'
const loggerConfig = defineConfig({
default: 'app',
/**
* The loggers object can be used to define multiple loggers.
* By default, we configure only one logger (named "app").
*/
loggers: {
app: {
enabled: true,
name: env.get('APP_NAME'),
level: env.get('LOG_LEVEL'),
transport: {
targets: targets()
.pushIf(!app.inProduction, targets.pretty())
.pushIf(app.inProduction, targets.file({ destination: 1 }))
.toArray(),
},
},
},
})
export default loggerConfig
/**
* Inferring types for the list of loggers you have configured
* in your application.
*/
declare module '@adonisjs/core/types' {
export interface LoggersList extends InferLoggers<typeof loggerConfig> {}
}

View File

@ -0,0 +1,48 @@
import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { defineConfig, stores } from '@adonisjs/session'
const sessionConfig = defineConfig({
enabled: true,
cookieName: 'adonis-session',
/**
* When set to true, the session id cookie will be deleted
* once the user closes the browser.
*/
clearWithBrowser: false,
/**
* Define how long to keep the session data alive without
* any activity.
*/
age: '2h',
/**
* Configuration for session cookie and the
* cookie store
*/
cookie: {
path: '/',
httpOnly: true,
secure: app.inProduction,
sameSite: 'lax',
},
/**
* The store to use. Make sure to validate the environment
* variable in order to infer the store name without any
* errors.
*/
store: env.get('SESSION_DRIVER'),
/**
* List of configured stores. Refer documentation to see
* list of available stores and their config.
*/
stores: {
cookie: stores.cookie(),
}
})
export default sessionConfig

View File

@ -0,0 +1,21 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'users'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id').notNullable()
table.string('full_name').nullable()
table.string('email', 254).notNullable().unique()
table.string('password').notNullable()
table.timestamp('created_at').notNullable()
table.timestamp('updated_at').nullable()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}

View File

@ -0,0 +1,20 @@
import User from '#models/user'
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import { faker } from '@faker-js/faker'
export default class extends BaseSeeder {
public async run () {
const usersExist = await User.query().first()
if (usersExist) {
console.log('Database already seeded, skipping...')
return
}
const users = Array.from({ length: 200 }).map(() => ({
fullName: faker.person.fullName(),
email: faker.internet.email(),
password: 'password123',
}))
await User.createMany(users)
}
}

View File

@ -0,0 +1,24 @@
#!/bin/sh
cd /app/backend
cp .env.example .env
echo "starting up..."
node ace generate:key
# Check for pending migrations by parsing output
PENDING_MIGRATIONS=$(node ace migration:status | grep -ic 'pending')
# Run migrations only if there are pending ones
if [ "$PENDING_MIGRATIONS" -gt 0 ]; then
echo "Found $PENDING_MIGRATIONS pending migration(s). Running migrations..."
node ace migration:run --force
else
echo "No pending migrations."
fi
echo "Seeding database..."
node ace db:seed
# Start the dev server
node ace serve --watch

View File

@ -0,0 +1,2 @@
import { configApp } from '@adonisjs/eslint-config'
export default configApp()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
{
"name": "backend",
"version": "0.0.0",
"private": true,
"type": "module",
"license": "UNLICENSED",
"scripts": {
"start": "node bin/server.js",
"build": "node ace build",
"dev": "node ace serve --hmr",
"test": "node ace test",
"lint": "eslint .",
"format": "prettier --write .",
"typecheck": "tsc --noEmit"
},
"imports": {
"#controllers/*": "./app/controllers/*.js",
"#exceptions/*": "./app/exceptions/*.js",
"#models/*": "./app/models/*.js",
"#mails/*": "./app/mails/*.js",
"#services/*": "./app/services/*.js",
"#listeners/*": "./app/listeners/*.js",
"#events/*": "./app/events/*.js",
"#middleware/*": "./app/middleware/*.js",
"#validators/*": "./app/validators/*.js",
"#providers/*": "./providers/*.js",
"#policies/*": "./app/policies/*.js",
"#abilities/*": "./app/abilities/*.js",
"#database/*": "./database/*.js",
"#start/*": "./start/*.js",
"#tests/*": "./tests/*.js",
"#config/*": "./config/*.js"
},
"devDependencies": {
"@adonisjs/assembler": "^7.8.2",
"@adonisjs/eslint-config": "^2.0.0",
"@adonisjs/prettier-config": "^1.4.4",
"@adonisjs/tsconfig": "^1.4.0",
"@japa/api-client": "^3.1.0",
"@japa/assert": "^4.0.1",
"@japa/plugin-adonisjs": "^4.0.0",
"@japa/runner": "^4.2.0",
"@swc/core": "1.11.24",
"@types/luxon": "^3.6.2",
"@types/node": "^22.15.18",
"eslint": "^9.26.0",
"hot-hook": "^0.4.0",
"pino-pretty": "^13.0.0",
"prettier": "^3.5.3",
"ts-node-maintained": "^10.9.5",
"typescript": "~5.8"
},
"dependencies": {
"@adonisjs/auth": "^9.4.0",
"@adonisjs/core": "^6.18.0",
"@adonisjs/cors": "^2.2.1",
"@adonisjs/lucid": "^21.6.1",
"@adonisjs/session": "^7.5.1",
"@faker-js/faker": "^9.8.0",
"@vinejs/vine": "^3.0.1",
"luxon": "^3.6.1",
"pg": "^8.16.0",
"reflect-metadata": "^0.2.2"
},
"hotHook": {
"boundaries": [
"./app/controllers/**/*.ts",
"./app/middleware/*.ts"
]
},
"prettier": "@adonisjs/prettier-config"
}

View File

@ -0,0 +1,38 @@
/*
|--------------------------------------------------------------------------
| Environment variables service
|--------------------------------------------------------------------------
|
| The `Env.create` method creates an instance of the Env service. The
| service validates the environment variables and also cast values
| to JavaScript data types.
|
*/
import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {
NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const),
PORT: Env.schema.number(),
APP_KEY: Env.schema.string(),
HOST: Env.schema.string({ format: 'host' }),
LOG_LEVEL: Env.schema.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']),
/*
|----------------------------------------------------------
| Variables for configuring database connection
|----------------------------------------------------------
*/
DB_HOST: Env.schema.string({ format: 'host' }),
DB_PORT: Env.schema.number(),
DB_USER: Env.schema.string(),
DB_PASSWORD: Env.schema.string.optional(),
DB_DATABASE: Env.schema.string(),
/*
|----------------------------------------------------------
| Variables for configuring session package
|----------------------------------------------------------
*/
SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const)
})

View File

@ -0,0 +1,44 @@
/*
|--------------------------------------------------------------------------
| HTTP kernel file
|--------------------------------------------------------------------------
|
| The HTTP kernel file is used to register the middleware with the server
| or the router.
|
*/
import router from '@adonisjs/core/services/router'
import server from '@adonisjs/core/services/server'
/**
* The error handler is used to convert an exception
* to an HTTP response.
*/
server.errorHandler(() => import('#exceptions/handler'))
/**
* The server middleware stack runs middleware on all the HTTP
* requests, even if there is no route registered for
* the request URL.
*/
server.use([
() => import('#middleware/container_bindings_middleware'),
() => import('#middleware/force_json_response_middleware'),
() => import('@adonisjs/cors/cors_middleware'),
])
/**
* The router middleware stack runs middleware on all the HTTP
* requests with a registered route.
*/
router.use([() => import('@adonisjs/core/bodyparser_middleware'), () => import('@adonisjs/session/session_middleware'), () => import('@adonisjs/auth/initialize_auth_middleware')])
/**
* Named middleware collection must be explicitly assigned to
* the routes or the routes group.
*/
export const middleware = router.named({
guest: () => import('#middleware/guest_middleware'),
auth: () => import('#middleware/auth_middleware')
})

View File

@ -0,0 +1,17 @@
/*
|--------------------------------------------------------------------------
| Routes file
|--------------------------------------------------------------------------
|
| The routes file is used for defining the HTTP routes.
|
*/
import router from '@adonisjs/core/services/router'
const UsersController = () => import('#controllers/users_controller')
router.get('users', [UsersController, 'index'])
router.get('/', async () => {
return {
hello: 'world',
}
})

View File

@ -0,0 +1,38 @@
import { assert } from '@japa/assert'
import { apiClient } from '@japa/api-client'
import app from '@adonisjs/core/services/app'
import type { Config } from '@japa/runner/types'
import { pluginAdonisJS } from '@japa/plugin-adonisjs'
import testUtils from '@adonisjs/core/services/test_utils'
/**
* This file is imported by the "bin/test.ts" entrypoint file
*/
/**
* Configure Japa plugins in the plugins array.
* Learn more - https://japa.dev/docs/runner-config#plugins-optional
*/
export const plugins: Config['plugins'] = [assert(), apiClient(), pluginAdonisJS(app)]
/**
* Configure lifecycle function to run before and after all the
* tests.
*
* The setup functions are executed before all the tests
* The teardown functions are executed after all the tests
*/
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
setup: [],
teardown: [],
}
/**
* Configure suites by tapping into the test suite instance.
* Learn more - https://japa.dev/docs/test-suites#lifecycle-hooks
*/
export const configureSuite: Config['configureSuite'] = (suite) => {
if (['browser', 'functional', 'e2e'].includes(suite.name)) {
return suite.setup(() => testUtils.httpServer().start())
}
}

View File

@ -0,0 +1,7 @@
{
"extends": "@adonisjs/tsconfig/tsconfig.app.json",
"compilerOptions": {
"rootDir": "./",
"outDir": "./build"
}
}

View File

@ -0,0 +1,17 @@
---
services:
backend:
build:
context: backend
target: develop
volumes:
- ./backend:/app/backend/
- node_modules:/app/backend/node_modules
- frontend_node_modules:/app/frontend/node_modules
- ./frontend/:/app/frontend/
depends_on:
db:
condition: service_healthy
volumes:
node_modules: {}
frontend_node_modules: {}

View File

@ -0,0 +1,47 @@
name: fullstack
services:
frontend:
image: frontend:latest
build:
context: ./frontend
target: develop
volumes:
- frontend_node_modules:/app/frontend/node_modules
- ./frontend/:/app/frontend/
labels:
- "traefik.http.routers.app.rule=Host(`app.docker.localhost`)"
reverse-proxy:
image: traefik:latest
command: --api.insecure=true --providers.docker
ports:
- 8888:80
- 8080:8080
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
backend:
image: backend:latest
build:
context: ./backend
labels:
- "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api"
- "traefik.http.routers.backend.rule=Host(`app.docker.localhost`) && PathPrefix(`/api`)"
- "traefik.http.routers.backend.middlewares=strip-api-prefix@docker"
depends_on:
db:
condition: service_healthy
db:
image: postgres:latest
environment:
- POSTGRES_PASSWORD=postgres
healthcheck:
test: pg_isready --d postgres --user postgres
interval: 5s
# By default docker compose will create a bridge network using the folder name where the docker-compose.yml
# resides and attach all containers in the compose file to that network. This is important because it allows
# for easier container to container networking.
# Isolate our node_modules from our host system. Docker will handle creating these volumes
volumes:
frontend_node_modules:
backend_node_modules:

View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -0,0 +1,16 @@
FROM node:22-alpine AS base
WORKDIR /app/frontend
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
FROM base AS build
RUN yarn build
FROM base AS develop
EXPOSE 5173
ENTRYPOINT [ "yarn", "dev", "--host", "0.0.0.0" ]
FROM nginx:alpine AS production
COPY --from=build /app/frontend/dist/ /usr/share/nginx/html
ENTRYPOINT [ "nginx", "-g", "daemon off;" ]

View File

@ -0,0 +1,54 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS + Docker + Traefik</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
{
"name": "vite-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,27 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -0,0 +1,80 @@
import { useState, useEffect } from 'react'
import reactLogo from './assets/react.svg'
import dockerLogo from './assets/docker.svg'
import './App.css'
function App() {
const [users, setUsers] = useState([])
const [page, setPage] = useState(1)
const [pagination, setPagination] = useState({
currentPage: 1,
lastPage: 1,
total: 0,
})
useEffect(() => {
fetch(`/api/users?page=${page}`)
.then(res => res.json())
.then(data => {
setUsers(data.data || [])
setPagination({
currentPage: data.meta.currentPage,
lastPage: data.meta.lastPage,
total: data.meta.total,
})
})
.catch(err => console.error('Failed to fetch users:', err))
}, [page])
const handlePrevious = () => {
if (page > 1) setPage(page - 1)
}
const handleNext = () => {
if (page < pagination.lastPage) setPage(page + 1)
}
return (
<>
<div>
<a href="https://react.dev" target="_blank" rel="noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
<a href="https://docker.com" target="_blank" rel="noreferrer">
<img src={dockerLogo} className="logo docker" alt="Docker logo" />
</a>
</div>
<h1>React + Docker + Traefik</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{users.length > 0 ? users.map((user) => (
<tr key={user.id}>
<td>{user.fullName}</td>
<td>{user.email}</td>
</tr>
)) : (
<tr>
<td colSpan="2">No users found</td>
</tr>
)}
</tbody>
</table>
<div className="pagination">
<button onClick={handlePrevious} disabled={page === 1}>Previous</button>
<span>Page {pagination.currentPage} of {pagination.lastPage}</span>
<button onClick={handleNext} disabled={page === pagination.lastPage}>Next</button>
</div>
</>
)
}
export default App

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2333.95 530.79">
<defs>
<style>
.cls-1 {
fill: #1d63ed;
stroke-width: 0px;
}
</style>
</defs>
<path class="cls-1" d="M661.56,218.08c-16.49-11.1-59.81-15.84-91.3-7.35-1.69-31.37-17.88-57.81-47.47-80.88l-10.95-7.35-7.3,11.03c-14.35,21.78-20.4,50.81-18.26,77.2,1.69,16.26,7.34,34.53,18.26,47.79-40.99,23.78-78.78,18.38-246.12,18.38H.06c-.75,37.79,5.32,110.47,51.54,169.64,5.11,6.54,10.7,12.86,16.78,18.95,37.58,37.63,94.36,65.23,179.26,65.3,129.53.12,240.5-69.9,308.01-239.18,22.21.36,80.85,3.98,109.55-51.47.7-.93,7.3-14.7,7.3-14.7l-10.94-7.35ZM168.67,183.53h-72.65v72.65h72.65v-72.65ZM262.52,183.53h-72.65v72.65h72.65v-72.65ZM356.38,183.53h-72.65v72.65h72.65v-72.65ZM450.24,183.53h-72.65v72.65h72.65v-72.65ZM74.81,183.53H2.16v72.65h72.65v-72.65ZM168.67,91.77h-72.65v72.65h72.65v-72.65ZM262.52,91.77h-72.65v72.65h72.65v-72.65ZM356.38,91.77h-72.65v72.65h72.65v-72.65ZM356.38,0h-72.65v72.65h72.65V0Z"/>
<g>
<path class="cls-1" d="M2329.44,419.3c0,18.94-14.87,33.81-34.21,33.81s-34.42-14.87-34.42-33.81,15.27-33.4,34.42-33.4,34.21,14.87,34.21,33.4ZM2269.37,419.3c0,14.87,11,26.68,26.07,26.68s25.46-11.81,25.46-26.47-10.8-26.89-25.65-26.89-25.87,12.02-25.87,26.68ZM2289.95,436.82h-7.74v-33.4c3.04-.61,7.33-1.02,12.82-1.02,6.32,0,9.16,1.02,11.61,2.45,1.84,1.42,3.26,4.07,3.26,7.33,0,3.67-2.85,6.52-6.91,7.74v.41c3.24,1.21,5.08,3.66,6.1,8.14,1.01,5.09,1.62,7.13,2.45,8.35h-8.35c-1.02-1.22-1.64-4.27-2.65-8.15-.61-3.66-2.65-5.29-6.93-5.29h-3.66v13.45ZM2290.14,417.88h3.66c4.28,0,7.74-1.42,7.74-4.88,0-3.06-2.23-5.11-7.13-5.11-2.03,0-3.46.21-4.27.43v9.56Z"/>
<path class="cls-1" d="M1017.16,81.28c-4.79-4.68-10.54-7.06-17.43-7.06s-12.81,2.38-17.42,7.06c-4.62,4.68-6.88,10.68-6.88,17.83v119.4c-23.7-19.59-51.05-29.47-82.16-29.47-36.16,0-67.08,13.06-92.7,39.27-25.62,26.12-38.34,57.72-38.34,94.78s12.81,68.57,38.34,94.78c25.62,26.12,56.46,39.27,92.7,39.27s66.74-13.06,92.7-39.27c25.62-25.86,38.34-57.45,38.34-94.78V99.11c0-7.15-2.35-13.15-7.15-17.83ZM968.98,355.39v.18c-4.27,10.15-10.11,19.06-17.51,26.65-7.4,7.68-16.12,13.68-26.05,18.18-10.02,4.5-20.65,6.71-32.06,6.71s-22.3-2.21-32.32-6.71c-10.02-4.5-18.65-10.5-25.96-18.09-7.32-7.59-13.15-16.5-17.42-26.65-4.27-10.24-6.45-21.09-6.45-32.57s2.18-22.33,6.45-32.57c4.27-10.24,10.11-19.06,17.42-26.65,7.32-7.59,16.03-13.59,25.96-18.09,10.02-4.5,20.74-6.71,32.32-6.71s22.04,2.21,32.06,6.71c10.02,4.5,18.65,10.5,26.05,18.18,7.4,7.68,13.24,16.59,17.51,26.65,4.27,10.15,6.45,20.92,6.45,32.39s-2.18,22.33-6.45,32.39Z"/>
<path class="cls-1" d="M2099.77,271.64c-6.36-15.89-16.05-30.27-28.76-43.16l-.17-.09c-25.88-26.12-56.82-39.27-92.7-39.27s-67.09,13.06-92.71,39.27c-25.62,26.12-38.33,57.72-38.33,94.78s12.81,68.57,38.33,94.78c25.62,26.12,56.47,39.27,92.71,39.27,32.92,0,61.41-10.85,85.64-32.56,4.69-4.94,7.06-10.94,7.06-17.92s-2.26-13.15-6.89-17.83c-4.61-4.68-10.45-7.06-17.42-7.06-6.09.18-11.5,2.21-16.11,6.27-7.32,6.35-15.25,11.21-23.87,14.39-8.63,3.18-18.04,4.77-28.31,4.77-9.07,0-17.78-1.41-26.05-4.32-8.29-2.91-16.03-6.89-22.92-12.09-6.98-5.21-12.98-11.38-18.12-18.71-5.14-7.24-9.06-15.27-11.67-24.09h185.32c6.87,0,12.62-2.38,17.42-7.06,4.8-4.68,7.15-10.68,7.15-17.83,0-18.53-3.24-35.74-9.58-51.54ZM1899.29,298.29c2.53-8.74,6.36-16.77,11.5-24.09,5.15-7.24,11.24-13.5,18.21-18.71,7.06-5.21,14.72-9.18,23.17-12.09,8.44-2.91,17.06-4.32,25.97-4.32s17.51,1.41,25.86,4.32c8.37,2.91,16.05,6.88,22.92,12.09,6.98,5.21,13.07,11.38,18.21,18.71,5.22,7.24,9.16,15.27,11.86,24.09h-157.71Z"/>
<path class="cls-1" d="M2327.51,205.89c-4.36-4.32-9.85-7.68-16.47-10.15-6.62-2.47-13.85-4.15-21.78-5.12-7.84-.97-15.25-1.41-22.12-1.41-15.61,0-30.24,2.56-44,7.68-13.77,5.12-26.49,12.44-38.17,21.97v-4.76c0-6.88-2.35-12.71-7.15-17.56-4.78-4.85-10.45-7.32-17.15-7.32s-12.64,2.47-17.42,7.32c-4.8,4.85-7.15,10.77-7.15,17.56v218.25c0,6.88,2.35,12.71,7.15,17.56,4.78,4.85,10.53,7.32,17.42,7.32s12.45-2.47,17.15-7.32c4.8-4.85,7.15-10.77,7.15-17.56v-109.17c0-11.65,2.18-22.59,6.45-32.83,4.27-10.24,10.11-19.06,17.51-26.65,7.42-7.59,16.13-13.59,26.05-17.92,10.02-4.41,20.66-6.62,32.08-6.62s22.2,2.03,32.06,6c3.91,1.77,7.32,2.65,10.28,2.65,3.4,0,6.62-.62,9.58-1.94,2.96-1.32,5.58-3.09,7.76-5.38,2.18-2.29,3.91-4.94,5.22-8.03,1.31-3,2.01-6.27,2.01-9.8,0-6.88-2.18-12.44-6.53-16.77h.08Z"/>
<path class="cls-1" d="M1304.49,271.73c-6.36-15.8-15.86-30.27-28.66-43.33-25.87-26.12-56.8-39.27-92.7-39.27s-67.08,13.06-92.7,39.27c-25.62,26.12-38.33,57.72-38.33,94.78s12.81,68.57,38.33,94.78c25.62,26.12,56.46,39.27,92.7,39.27s66.74-13.06,92.7-39.27c25.62-25.86,38.34-57.45,38.34-94.78-.18-18.53-3.4-35.65-9.67-51.45ZM1258.84,355.39v.18c-4.27,10.15-10.11,19.06-17.51,26.65-7.4,7.68-16.12,13.68-26.05,18.18-9.93,4.5-20.65,6.71-32.06,6.71s-22.3-2.21-32.32-6.71c-10.02-4.5-18.65-10.5-25.96-18.09-7.32-7.59-13.15-16.5-17.42-26.65-4.27-10.24-6.45-21.09-6.45-32.57s2.18-22.33,6.45-32.57c4.27-10.24,10.11-19.06,17.42-26.65,7.32-7.59,16.03-13.59,25.96-18.09,10.02-4.5,20.74-6.71,32.32-6.71s22.04,2.21,32.06,6.71c10.02,4.5,18.65,10.5,26.05,18.18,7.4,7.68,13.24,16.59,17.51,26.65,4.27,10.15,6.45,20.92,6.45,32.39s-2.18,22.33-6.45,32.39Z"/>
<path class="cls-1" d="M1828.62,214.01c0-3.35-.7-6.53-2-9.53-1.31-3-3.05-5.73-5.23-8.03-2.18-2.29-4.79-4.15-7.75-5.38-2.96-1.23-6.18-1.94-9.58-1.94-4.88,0-9.24,1.24-13.07,3.8l-139.92,93.11V99.29c0-7.06-2.35-12.97-7.14-17.83-4.79-4.85-10.45-7.32-17.16-7.32s-12.63,2.47-17.43,7.32c-4.79,4.85-7.14,10.77-7.14,17.83v332.71c0,6.88,2.35,12.8,7.14,17.74,4.79,4.94,10.54,7.41,17.43,7.41s12.46-2.47,17.16-7.41c4.79-4.94,7.14-10.86,7.14-17.74v-86.4l28.58-19.15,108.12,124.17c4.36,4.32,9.85,6.44,16.38,6.44,3.4,0,6.62-.62,9.58-1.94,2.96-1.24,5.58-3.09,7.75-5.38,2.18-2.29,3.92-4.94,5.23-8.03,1.31-3,2-6.27,2-9.53,0-6.53-2.26-12.36-6.8-17.47l-100.63-115.87,98.01-65.13c6.27-4.32,9.32-10.94,9.32-19.86v.18Z"/>
<path class="cls-1" d="M1414.36,263.7c7.49-7.59,16.21-13.59,26.23-17.92,10.02-4.41,20.65-6.62,32.06-6.62,10.28,0,19.78,1.77,28.58,5.29,8.71,3.53,17.08,8.74,25,15.53,4.7,3.79,10.02,5.73,15.94,5.73,7.06,0,12.81-2.38,17.43-7.15,4.62-4.77,6.88-10.77,6.88-17.92s-2.79-13.77-8.45-18.88c-24.05-21.71-52.53-32.57-85.38-32.57-36.16,0-67.08,13.06-92.7,39.27-25.62,26.12-38.33,57.72-38.33,94.78s12.81,68.57,38.33,94.78c25.62,26.12,56.46,39.27,92.7,39.27,32.76,0,61.25-10.85,85.38-32.57,5.14-5.29,7.76-11.38,7.76-18.44s-2.27-13.15-6.88-17.83c-4.62-4.68-10.45-7.06-17.42-7.06-5.92.18-11.07,1.94-15.42,5.29-7.84,6.88-16.03,12-24.83,15.44-8.71,3.44-18.21,5.12-28.58,5.12-11.41,0-22.04-2.21-32.06-6.62-10.02-4.41-18.73-10.41-26.23-17.91-7.49-7.5-13.42-16.5-17.69-26.65-4.27-10.24-6.45-21.18-6.45-32.83s2.18-22.59,6.45-32.83c4.27-10.24,10.19-19.06,17.69-26.65v-.09Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Binary file not shown.

11
slides/README.md Normal file
View File

@ -0,0 +1,11 @@
# Welcome to [Slidev](https://github.com/slidevjs/slidev)!
To start the slide show:
- `pnpm install`
- `pnpm dev`
- visit <http://localhost:3030>
Edit the [slides.md](./slides.md) to see the changes.
Learn more about Slidev at the [documentation](https://sli.dev/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
slides/images/talk.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

View File

@ -12,5 +12,6 @@
"@slidev/theme-default": "latest",
"@slidev/theme-seriph": "latest",
"vue": "^3.5.16"
}
}
},
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}

View File

@ -5,15 +5,13 @@ theme: seriph
# like them? see https://unsplash.com/collections/94734566/slidev
background: https://cover.sli.dev
# some information about your slides (markdown enabled)
title: Welcome to Slidev
title: Demystifying Docker - SCS 2025
info: |
## Slidev Starter Template
Presentation slides for developers.
## The basics of containerization and beyond by Mike Conrad
Scenic City Summit 2025
Learn more at [Sli.dev](https://sli.dev)
# apply unocss classes to the current slide
Learn more at [Hackanooga](https://hackanooga.com/scs)
class: text-center
# https://sli.dev/features/drawing
drawings:
persist: false
# slide transition: https://sli.dev/guide/animations.html#slide-transitions
@ -29,8 +27,13 @@ mdc: true
Mike Conrad - SCS 2025
<div @click="$slidev.nav.next" class="mt-12 py-1" hover:bg="white op-10">
Press Space for next page <carbon:arrow-right />
<div @click="$slidev.nav.next" class="mt-12 py-1 flex justify-center flex-col">
<!-- Press Space for next page <carbon:arrow-right /> -->
Follow along
<p><a href="https://hackanooga.com/scs">https://hackanooga.com/scs</a></p>
<small>Includes slide deck, and repo with examples</small>
<img src="./images/talk.jpg" width="200">
Connect with me on LinkedIn: https://linkedin.com/enxoco
</div>
<div class="abs-br m-6 text-xl">
@ -46,45 +49,67 @@ Mike Conrad - SCS 2025
The last comment block of each slide will be treated as slide notes. It will be visible and editable in Presenter Mode along with the slide. [Read more in the docs](https://sli.dev/guide/syntax.html#notes)
-->
---
transition: fade-out
layout: statement
background: ./images/pexels-markusspiske-1089438.jpg
---
# The 3 universal constants in programming
<v-click>
<h2>1) The speed of light</h2>
</v-click>
<v-click>
<h2>2) "It's more complicated than you think"</h2>
</v-click>
<v-click>
<h2>3) "It works on my machine"</h2>
<br />
<small>Source: <a href="https://www.linkedin.com/posts/robertroskam_the-3-universal-constants-in-programming-activity-7339260450074775553-ofik?utm_source=share&utm_medium=member_desktop&rcm=ACoAACZXneYB_uWiOE0T9VO3caUkn7m0ZMrRS_o">Some random guy on the internet</a></small>
</v-click>
---
transition: fade-out
layout: center
---
<img src="./images/docker-meme.jpg" width="300"/>
---
transition: fade-out
layout: image-right
image: ./images/pexels-markusspiske-1089438.jpg
---
# Who is this for?
## About you
- Some experience with Docker/containers
- Some experience with BASH
- Familiarity with Linux/BASH/zsh, etc
- Want to better understand how containers work
- Want to learn new techniques for automation
---
transition: fade-out
layout: center
layout: image-left
image: ./images/pexels-joshsorenson-1714208.jpg
---
## Follow Along
**Example Repo** - https://git.hackanooga.com/mikeconrad/demystifying-docker
<small>Visit the link to check out the sample Git repository.</small>
---
transition: fade-out
layout: center
---
<img src="https://miro.medium.com/v2/resize:fit:400/format:webp/1*Ibnwjo9LtUFxRY1MZgOcvg.png"/>
---
transition: fade-out
layout: center
---
## Common Use cases for containers
- Reproducible dev environments
- Testing in CI/CD environments
- Better "Portability" of application code
- Snapshot of application code at specific point in time
**Example Repo** - https://hackanooga.com/scs
**Prerequisites**
- Docker Engine (Linux) or Docker Desktop (Windows/MacOS)
- VSCode
- Git
- yarn, npm or pnpm (for viewing slides)
### VSCode plugins
- [Official Docker Plugin](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)
- [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
- [Container Tools](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-containers)
---
transition: fade-out
@ -109,13 +134,13 @@ layout: center
## Containers vs Virtual Machines
| Feature | VM | Container |
|------------------|----------------|------------------|
| Boot time | Minutes | Seconds |
| Resource usage | Heavy | Lightweight |
| Isolation | Strong | Process-level |
| Portability | Medium | Very High |
| Feature | VMs | Containers |
|------------------|-----------------------|------------------------------|
| Boot time | Minutes | Seconds |
| Resource usage | Heavy | Lightweight |
| Isolation | Strong | Process-level |
| Portability | Medium | Very High |
| Operating System | Needs full OS install | Uses host OS/kernel features |
In reality we use containers and vm's together. Containers run inside of VM's for better security and isolation, especially in cloud and multi tenant environments.
---
@ -125,36 +150,17 @@ layout: center
## What is Docker?
- A tool to build and run containers
- Written in GO
- Uses Client/Server model with REST API (`docker cli` and `dockerd`)
- Eco system of tools (Compose, Swarm, etc)
- Public Image Registry (Dockerhub)
- Docker client typically runs on same machine as server but doesn't have to
---
transition: fade-out
layout: center
---
## What is Docker?
- A tool to build and run containers
- Containers are exclusive to Linux
- Docker engine runs containers using Linux features like:
- Namespaces
- cgroups
- Union file systems
- Container runs from an image layered with base image and application code
---
transition: fade-out
layout: center
---
## Common Use Cases
- Reproducible Dev environments (dev containers)
- Preview/PR environments (ephemeral test environments)
- Legacy applications or applications with complex environment setups
- Uses Linux kernal features like:
- Namespaces
- cgroups
- Union file systems
- Containers are just processes
---
transition: fade-out
layout: center
@ -186,24 +192,42 @@ layout: center
---
transition: fade-out
layout: center
layout: two-cols-header
---
## Bind/Volume Mounts
<div class="flex items-center flex-col">
<h1 class="ml-5">Bind/Volume Mounts</h1>
- 2 most common storage mechanisms
- Different use cases and security implications
<p>2 most common storage mechanisms<br />Different use cases and security implications</p>
</div>
::left::
## Bind Mounts
- Created/managed by user.
- Files from host mounted directly into container.
- Container processes can modify files on host system.
- Strongly tied to the host.
- Best for things like dev containers.
::right::
## Volume mounts
- Created/managed by Docker Daemon.
- Data is stored on host filesystem.
- Used for persistent data.
<!--
It is possible to modify the data directly via normal tools but unsupported and can cause unintended side-effects due to the overlayfs storage driver.
An example would be creating a postgres volume for persistent database storage.
-->
---
transition: fade-out
layout: center
image: 'https://unsplash.com/collections/oGE7TYSLt3I/software-development
equal: false
left: false
---
## Bind Mounts
- Mounting files/directories from the host machine directly into a container (merged overlayfs layer).
- Processes inside container can modify files on host system.
- Bind mounts are strongly tied to the host
- Best for things like dev containers where you need to mount source code into container and have hot reload, etc.
## Bind Mount Example
```bash
@ -257,20 +281,6 @@ $ docker volume inspect postgresData
- Docker creates a volume named postgresData and mounts that directory inside the container.
<!-- https://docs.docker.com/engine/storage/bind-mounts/ -->
---
transition: fade-out
layout: center
---
## Volume mounts
- Created and managed by the Docker Daemon
- Volume data is stored on host filesystem but managed by Docker.
- Used for persistent data.
<!--
It is possible to modify the data directly via normal tools but unsupported and can cause unintended side-effects due to the overlayfs storage driver.
An example would be creating a postgres volume for persistent database storage.
-->
---
transition: fade-out
@ -323,7 +333,7 @@ EXPOSE 5173
```bash
$ docker build -t react .
$ docker run --rm -P react
$ docker run --rm -p 5173:5173 react
```
<!--
Run docker image and demonstrate dev container functionality. Attach to the running container
@ -340,6 +350,7 @@ layout: center
- Define multi-container apps in one file
- Great for local dev and staging (and production!)
- Glue together multiple services with networking
---
transition: fade-out
@ -356,11 +367,11 @@ layout: center
---
## Resources
- [Slide Deck (including examples)](https://git.hackanooga.com/mikeconrad/demystifying-docker-v2)
- [Slide Deck (including examples)](https://git.hackanooga.com/mikeconrad/demystifying-docker)
- [DocketProxy (Docker socket proxy)](https://git.hackanooga.com/mikeconrad/docketproxy)
- [SlimToolkit (Optimize and secure containers)](https://github.com/slimtoolkit/slim)
## VSCode plugins
https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker
https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers
https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-containers
- [Official Docker Plugin](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)
- [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
- [Container Tools](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-containers)