├── README.md ├── src ├── assets │ ├── placeholder_1.jpg │ ├── moon.svg │ ├── sun.svg │ └── sun copy.svg ├── utils │ ├── common.ts │ ├── setUserPreferences.ts │ ├── getUserPreferences.ts │ ├── setTheme.ts │ └── setup.ts ├── components │ ├── ThemeToggler.astro │ ├── Menu.astro │ ├── Icon.astro │ └── HeaderLink.astro ├── content │ └── posts.ts ├── pages │ ├── posts │ │ └── [slug].astro │ ├── index.astro │ ├── about.astro │ └── cv.astro ├── layouts │ └── BaseLayout.astro └── styles │ ├── global.css │ └── animations.css ├── .vscode ├── extensions.json └── launch.json ├── public ├── FabioOliveiraCostaCV.pdf ├── moon.svg ├── sun.svg └── favicon.svg ├── tsconfig.json ├── astro.config.mjs ├── .gitignore ├── package.json ├── scripts ├── getDevToPosts │ └── index.ts └── data │ └── allPosts.json └── .github └── workflows └── deploy.yml /README.md: -------------------------------------------------------------------------------- 1 | # Portfolio site 2 | 3 | Made with astro and tailwind -------------------------------------------------------------------------------- /src/assets/placeholder_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drFabio/fabiooliveiracosta/main/src/assets/placeholder_1.jpg -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /public/FabioOliveiraCostaCV.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drFabio/fabiooliveiracosta/main/public/FabioOliveiraCostaCV.pdf -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"] 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const themes = ['light', 'dark'] as const 2 | export type Theme = typeof themes[number] 3 | 4 | export type Preferences = { 5 | theme?: Theme 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'astro/config'; 3 | 4 | import tailwindcss from '@tailwindcss/vite'; 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | site: 'https://drfabio.github.io', 9 | base: '/fabiooliveiracosta', 10 | vite: { 11 | plugins: [tailwindcss()] 12 | } 13 | }); -------------------------------------------------------------------------------- /src/components/ThemeToggler.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Icon from "./Icon.astro"; 3 | --- 4 | 5 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /src/utils/setUserPreferences.ts: -------------------------------------------------------------------------------- 1 | import type { Preferences } from "./common"; 2 | import { getUserPreferences } from "./getUserPreferences"; 3 | 4 | export function setUserPreferences(newPreferences: Partial) { 5 | const oldPreferences = getUserPreferences() 6 | window.localStorage.setItem('preferences', JSON.stringify({ ...oldPreferences, ...newPreferences })) 7 | 8 | } -------------------------------------------------------------------------------- /public/moon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /src/assets/moon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /public/sun.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /src/assets/sun.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /src/assets/sun copy.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /src/utils/getUserPreferences.ts: -------------------------------------------------------------------------------- 1 | import type { Preferences } from "./common"; 2 | 3 | export function getUserPreferences() { 4 | 5 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 6 | const serialized = localStorage.getItem('preferences') 7 | const savedPreferences: Preferences = JSON.parse(serialized || `{}`) 8 | 9 | return { theme: prefersDark ? 'dark' : 'light', ...savedPreferences }; 10 | } -------------------------------------------------------------------------------- /src/content/posts.ts: -------------------------------------------------------------------------------- 1 | import { data } from '../../scripts/data/allPosts.json' 2 | 3 | export interface Post { 4 | title: string; 5 | date: string; 6 | description: string; 7 | imageUrl?: string | null; 8 | slug: string; 9 | url: string; 10 | } 11 | 12 | export const posts: Post[] = data.map((d) => ({ 13 | title: d.title, 14 | date: d.published_timestamp, 15 | description: d.description, 16 | imageUrl: d.cover_image, 17 | slug: d.slug, 18 | url: d.url 19 | } satisfies Post)) 20 | 21 | -------------------------------------------------------------------------------- /src/utils/setTheme.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from "./common"; 2 | import { setUserPreferences } from "./setUserPreferences"; 3 | 4 | export function setTheme(shouldBeDark: boolean = true) { 5 | 6 | const currentTheme: Theme = shouldBeDark ? 'dark' : 'light' 7 | const oldTheme: Theme = shouldBeDark ? 'light' : 'dark' 8 | document.documentElement.classList.remove(oldTheme); 9 | document.documentElement.classList.add(currentTheme); 10 | 11 | setUserPreferences({ theme: currentTheme }) 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "build": "astro build", 8 | "preview": "astro preview", 9 | "astro": "astro", 10 | "build:posts": "ts-node scripts/getDevToPosts/index.ts" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/vite": "^4.1.17", 14 | "astro": "^5.16.4", 15 | "node-html-parser": "^7.0.1", 16 | "tailwindcss": "^4.1.17" 17 | }, 18 | "devDependencies": { 19 | "ts-node": "^10.9.2" 20 | } 21 | } -------------------------------------------------------------------------------- /src/utils/setup.ts: -------------------------------------------------------------------------------- 1 | import { getUserPreferences } from './getUserPreferences' 2 | import { setTheme } from './setTheme'; 3 | import { themes, type Theme } from './common' 4 | 5 | 6 | function toogleTheme(_e: PointerEvent) { 7 | const currentTheme = Array.from(document.documentElement.classList).filter((theme: string) => themes.includes(theme as Theme))?.[0] ?? 'dark'; 8 | setTheme(currentTheme !== 'dark'); 9 | } 10 | function setup() { 11 | const { theme } = getUserPreferences() 12 | setTheme(theme === 'dark'); 13 | document.getElementById('theme-toggle')?.addEventListener("click", toogleTheme) 14 | } 15 | 16 | 17 | setup() -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/Menu.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ThemeToggler from "./ThemeToggler.astro"; 3 | import HeaderLink from "./HeaderLink.astro"; 4 | 5 | const base = import.meta.env.BASE_URL!; 6 | --- 7 | 8 |
9 | 20 |
21 | -------------------------------------------------------------------------------- /src/components/Icon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { parse } from "node-html-parser"; 3 | import type { HTMLAttributes } from "astro/types"; 4 | 5 | export interface Props extends HTMLAttributes<"svg"> { 6 | icon: string; 7 | } 8 | 9 | const { icon, ...attributes } = Astro.props as Props; 10 | const { default: originalSvg } = await import(`../assets/${icon}.svg?raw`); 11 | 12 | // const innerHTML = addAttributesToInnerHTMLString(svg, attributes); 13 | const root = parse(originalSvg); 14 | const svg = root.querySelector("svg"); 15 | 16 | const { attributes: svgAttributes, innerHTML } = svg!; 17 | 18 | const mergedAttributes = Object.entries(attributes).reduce( 19 | (acc, [key, value]) => { 20 | if (key === "class") { 21 | return { ...acc, [key]: `${value} ${acc?.class ?? ""}`.trim() }; 22 | } 23 | return { ...acc, [key]: value }; 24 | }, 25 | svgAttributes, 26 | ); 27 | --- 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/HeaderLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from "astro/types"; 3 | 4 | type Props = HTMLAttributes<"a">; 5 | 6 | const { href, class: className, ...props } = Astro.props; 7 | const base = import.meta.env.BASE_URL!; 8 | 9 | const pathname = Astro.url.pathname 10 | .replace(import.meta.env.BASE_URL, "") 11 | .replace(/^\/+|\/+$/g, ""); 12 | const subpath = pathname.match(/[^\/]+/g); 13 | const sanitizedHref = `${href}`.replace(base, "").replace(/^\/+|\/+$/g, ""); 14 | 15 | const isActive = 16 | sanitizedHref === pathname || href === "/" + (subpath?.[0] || ""); 17 | const defaultClasses: any = [ 18 | "text-sm", 19 | "font-medium", 20 | "text-gray-700", 21 | "dark:text-gray-300", 22 | "hover:text-gray-900", 23 | "dark:hover:text-white", 24 | "transition-colors", 25 | "text-glow", 26 | ]; 27 | --- 28 | 29 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /scripts/getDevToPosts/index.ts: -------------------------------------------------------------------------------- 1 | import { ok } from 'node:assert' 2 | import { writeFile, readFile } from 'node:fs/promises' 3 | import { join, dirname } from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | const __filename = fileURLToPath(import.meta.url) 7 | const __dirname = dirname(__filename) 8 | 9 | const POST_PER_PAGE = 30 10 | /** 11 | * @see{https://developers.forem.com/api/v1#tag/articles/operation/getArticles} 12 | * @todo do pagination and check lates post 13 | */ 14 | async function getPosts() { 15 | ok(process.env.DEV_TO_API_KEY, ' DEV_TO_API_KEY is missing') 16 | const response = await fetch('https://dev.to/api/articles/me/published', { 17 | headers: { 18 | 'api-key': process.env.DEV_TO_API_KEY 19 | } 20 | }) 21 | const data = await response.json() 22 | 23 | const payload = { data } 24 | const filePath = join(__dirname, '../data/allPosts.json') 25 | 26 | await writeFile(filePath, JSON.stringify(payload, null, 4), 'utf-8') 27 | console.log(`Saved ${data.length} posts to ${filePath}`) 28 | } 29 | 30 | getPosts().then(() => console.log('Done')).catch((err) => console.error(err)) -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | # Trigger the workflow every time you push to the `main` branch 5 | # Using a different branch name? Replace `main` with your branch’s name 6 | push: 7 | branches: [ main ] 8 | # Allows you to run this workflow manually from the Actions tab on GitHub. 9 | workflow_dispatch: 10 | 11 | # Allow this job to clone the repo and create a page deployment 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout your repository using git 22 | uses: actions/checkout@v5 23 | - name: Install, build, and upload your site 24 | uses: withastro/action@v5 25 | # with: 26 | # path: . # The root location of your Astro project inside the repository. (optional) 27 | # node-version: 24 # The specific version of Node that should be used to build your site. Defaults to 22. (optional) 28 | # package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional) 29 | # build-cmd: pnpm run build # The command to run to build your site. Runs the package build script/task by default. (optional) 30 | # env: 31 | # PUBLIC_POKEAPI: 'https://pokeapi.co/api/v2' # Use single quotation marks for the variable value. (optional) 32 | 33 | deploy: 34 | needs: build 35 | runs-on: ubuntu-latest 36 | environment: 37 | name: github-pages 38 | url: ${{ steps.deployment.outputs.page_url }} 39 | steps: 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /src/pages/posts/[slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../../layouts/BaseLayout.astro"; 3 | import { posts } from "../../content/posts"; 4 | 5 | export async function getStaticPaths() { 6 | return posts.map((post) => ({ 7 | params: { slug: post.slug }, 8 | props: { post }, 9 | })); 10 | } 11 | 12 | const { post } = Astro.props; 13 | --- 14 | 15 | 16 |
17 | 21 | 27 | 32 | 33 | Back to Home 34 | 35 | 36 |
37 | 46 |

49 | {post.title} 50 |

51 |
52 | 53 | {post.title} 58 | 59 |
60 |

63 | {post.description} 64 |

65 |
66 |
67 |
68 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import "../styles/global.css"; 3 | import Menu from "../components/Menu.astro"; 4 | 5 | interface Props { 6 | title: string; 7 | description?: string; 8 | } 9 | 10 | const { title, description = "Developer Portfolio" } = Astro.props; 11 | const theme = "dark"; 12 | --- 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {title} 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "./animations.css"; 3 | @custom-variant dark (&:where(.dark, .dark *)); 4 | 5 | :root { 6 | color-scheme: light; 7 | 8 | /* Surfaces */ 9 | --color-bg: #f9fafb; 10 | /* slate-50 */ 11 | --color-surface: #ffffff; 12 | 13 | /* Text */ 14 | --color-text: #020617; 15 | /* slate-950 / near-black for stronger contrast */ 16 | --color-text-muted: #4b5563; 17 | /* gray-700 for better legibility */ 18 | 19 | /* Borders */ 20 | --color-border: #e5e7eb; 21 | /* gray-200 */ 22 | --color-border-strong: #d1d5db; 23 | /* gray-300 */ 24 | 25 | /* Accents */ 26 | --color-accent: #2563eb; 27 | /* blue-600 */ 28 | --color-accent-soft: rgba(37, 99, 235, 0.08); 29 | 30 | /* Header */ 31 | --color-header-bg: rgba(40, 1, 124, 0.8); 32 | --color-header-border: rgba(229, 231, 235, 1); 33 | --_shadow-size: 1.5em; 34 | --_shadow-color: #0065ff; 35 | --_shadow-color-off: #000; 36 | 37 | --_pulse-duration: 1s; 38 | } 39 | 40 | :root.light { 41 | .on-dark { 42 | display: none; 43 | } 44 | } 45 | 46 | :root.dark { 47 | --_shadow-color: #f09; 48 | --_shadow-color-off: #fff; 49 | --_pulse-duration: 0.11s; 50 | 51 | color-scheme: dark; 52 | 53 | /* Surfaces */ 54 | --color-bg: #020617; 55 | /* slate-950 */ 56 | --color-surface: #020617; 57 | 58 | /* Text */ 59 | --color-text: #e5e7eb; 60 | /* gray-200 */ 61 | --color-text-muted: #9ca3af; 62 | /* gray-400 */ 63 | 64 | /* Borders */ 65 | --color-border: #1f2937; 66 | /* gray-800 */ 67 | --color-border-strong: #111827; 68 | /* gray-900 */ 69 | 70 | /* Accents */ 71 | --color-accent: #38bdf8; 72 | /* sky-400 */ 73 | --color-accent-soft: rgba(56, 189, 248, 0.12); 74 | 75 | /* Header */ 76 | --color-header-bg: rgba(15, 23, 42, 0.85); 77 | --color-header-border: rgba(31, 41, 55, 1); 78 | 79 | .on-light { 80 | display: none; 81 | } 82 | } 83 | 84 | body { 85 | font-feature-settings: "kern" 1, "liga" 1; 86 | -webkit-font-smoothing: antialiased; 87 | -moz-osx-font-smoothing: grayscale; 88 | 89 | background-color: var(--color-bg); 90 | color: var(--color-text); 91 | } 92 | 93 | button:hover, 94 | a:hover { 95 | cursor: pointer; 96 | } 97 | 98 | html { 99 | scroll-behavior: smooth; 100 | } 101 | 102 | a.active { 103 | font-weight: bolder; 104 | text-decoration: underline; 105 | } -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../layouts/BaseLayout.astro"; 3 | import { posts } from "../content/posts"; 4 | import placeholder from "../assets/placeholder_1.jpg"; 5 | 6 | const base = import.meta.env.BASE_URL!; 7 | --- 8 | 9 | 10 | 11 |
12 |
13 |

16 | Developer Portfolio 17 |

18 |

21 | Building modern web applications with passion and precision 22 |

23 | 37 |
38 |
39 | 40 | 41 |
42 |

43 | Recent Posts 44 |

45 | 81 |
82 |
83 | -------------------------------------------------------------------------------- /src/styles/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes pulsate_text { 2 | 100% { 3 | /* Larger blur radius */ 4 | text-shadow: 5 | 0 0 4px var(--_shadow-color-off), 6 | 0 0 11px var(--_shadow-color-off), 7 | 0 0 19px var(--_shadow-color-off), 8 | 0 0 40px var(--_shadow-color), 9 | 0 0 80px var(--_shadow-color), 10 | 0 0 90px var(--_shadow-color), 11 | 0 0 100px var(--_shadow-color), 12 | 0 0 150px var(--_shadow-color); 13 | } 14 | 15 | 0% { 16 | /* A slightly smaller blur radius */ 17 | text-shadow: 18 | 0 0 4px var(--_shadow-color-off), 19 | 0 0 10px var(--_shadow-color-off), 20 | 0 0 18px var(--_shadow-color-off), 21 | 0 0 38px var(--_shadow-color), 22 | 0 0 73px var(--_shadow-color), 23 | 0 0 80px var(--_shadow-color), 24 | 0 0 94px var(--_shadow-color), 25 | 0 0 140px var(--_shadow-color); 26 | } 27 | } 28 | 29 | @keyframes pulsate_text_reverse { 30 | 100% { 31 | /* Larger blur radius */ 32 | text-shadow: 33 | 0 0 4px var(--_shadow-color), 34 | 0 0 11px var(--_shadow-color), 35 | 0 0 19px var(--_shadow-color), 36 | 0 0 40px var(--_shadow-color-off), 37 | 0 0 80px var(--_shadow-color-off), 38 | 0 0 90px var(--_shadow-color-off), 39 | 0 0 100px var(--_shadow-color-off), 40 | 0 0 150px var(--_shadow-color-off); 41 | } 42 | 43 | 0% { 44 | /* A slightly smaller blur radius */ 45 | text-shadow: 46 | 0 0 4px var(--_shadow-color), 47 | 0 0 10px var(--_shadow-color), 48 | 0 0 18px var(--_shadow-color), 49 | 0 0 38px var(--_shadow-color-off), 50 | 0 0 73px var(--_shadow-color-off), 51 | 0 0 80px var(--_shadow-color-off), 52 | 0 0 94px var(--_shadow-color-off), 53 | 0 0 140px var(--_shadow-color-off); 54 | } 55 | } 56 | 57 | @keyframes pulsate_box { 58 | 100% { 59 | /* Larger blur radius */ 60 | box-shadow: 61 | 0 0 4px var(--_shadow-color-off), 62 | 0 0 11px var(--_shadow-color-off), 63 | 0 0 19px var(--_shadow-color-off), 64 | 0 0 40px var(--_shadow-color), 65 | 0 0 80px var(--_shadow-color), 66 | 0 0 90px var(--_shadow-color), 67 | 0 0 100px var(--_shadow-color), 68 | 0 0 150px var(--_shadow-color); 69 | } 70 | 71 | 0% { 72 | /* A slightly smaller blur radius */ 73 | box-shadow: 74 | 0 0 4px var(--_shadow-color-off), 75 | 0 0 10px var(--_shadow-color-off), 76 | 0 0 18px var(--_shadow-color-off), 77 | 0 0 38px var(--_shadow-color), 78 | 0 0 73px var(--_shadow-color), 79 | 0 0 80px var(--_shadow-color), 80 | 0 0 94px var(--_shadow-color), 81 | 0 0 140px var(--_shadow-color); 82 | } 83 | } 84 | 85 | .text-glow:active, 86 | .text-glow:hover, 87 | .text-glow:focus { 88 | animation: pulsate_text var(--_pulse-duration) ease-in-out infinite alternate; 89 | 90 | } 91 | 92 | .light { 93 | 94 | .text-glow:active, 95 | .text-glow:hover, 96 | .text-glow:focus { 97 | animation: pulsate_text_reverse var(--_pulse-duration) ease-in-out infinite alternate; 98 | 99 | } 100 | } 101 | 102 | 103 | .box-glow:active, 104 | .box-glow:hover, 105 | .box-glow:focus { 106 | animation: pulsate_box var(--_pulse-duration) ease-in-out infinite alternate; 107 | 108 | } 109 | 110 | @media screen and (prefers-reduced-motion) { 111 | 112 | .box-glow:active, 113 | .box-glow:hover, 114 | .box-glow:focus, 115 | .text-glow:active, 116 | .text-glow:hover, 117 | .text-glow:focus { 118 | animation: none; 119 | } 120 | } -------------------------------------------------------------------------------- /src/pages/about.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../layouts/BaseLayout.astro"; 3 | --- 4 | 5 | 9 |
10 |

13 | About Me 14 |

15 | 16 |
17 |

20 | I am polyglot developer with {new Date().getFullYear() - 2004} years 21 | of experience, I've been all over the place but TypeScript and React 22 | has been the bulk of my latest experiences lately. 23 |

24 |

25 | I started working when I was 15 years old with a PHP book and a 26 | Technical Degree, eventually I learned a lot by doing. I was 27 | able to fit a Baschelor on computer science and one of Science 28 | and technology while still working. I had a broad career seeing 29 | technologies bloom and fade away, and adapting myself at every 30 | step. 31 |

32 |

33 | Currently I value testing, a tight feedback loop, and 34 | experimental development. Developers should be able to test 35 | their hypothesis often, safely and quickly. A fast and safe Dev 36 | is a happy dev. 37 |

38 |

39 | On a personal level I enjoy close-up Magic, Bouldering, Board 40 | games and nice beers. 41 |

42 |
43 | 44 |
45 |

46 | Current Stack 47 |

48 |
49 |
52 |

55 | Frontend 56 |

57 |
    60 |
  • React
  • 61 |
  • CSS
  • 62 |
  • Cypress
  • 63 |
  • Storybook
  • 64 |
65 |
66 |
69 |

72 | Backend 73 |

74 |
    77 |
  • Next.js
  • 78 |
  • NestJs
  • 79 |
  • PostgreSQL
  • 80 |
  • Graphql
  • 81 |
82 |
83 |
86 |

89 | Infrastructure 90 |

91 |
    94 |
  • CDK
  • 95 |
  • Docker
  • 96 |
  • AWS
  • 97 |
98 |
99 |
100 |
101 | 102 |
103 |

104 | Current position 105 |

106 |
107 |
108 |

111 | Staff Engineer 112 |

113 |

114 | Taxdoo • January 2025 - Present 115 |

116 |

117 | Leading development of modern web applications using 118 | React and TypeScript. Collaborating with 119 | cross-functional teams to deliver high-quality software 120 | solutions. 121 |

122 |
123 |
124 |
125 | 126 | 134 |
135 |
136 | -------------------------------------------------------------------------------- /src/pages/cv.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../layouts/BaseLayout.astro"; 3 | --- 4 | 5 | 9 |
10 | 11 |
14 |
15 |

18 | Fabio Oliveira Costa 19 |

20 |

21 | Staff Engineer · Tech Lead · Polyglot Developer 22 |

23 |
24 | 29 | 35 | 41 | 42 | Download PDF 43 | 44 |
45 | 46 | 47 |
48 |

51 | About me 52 |

53 |

54 | I am a polyglot developer with over 21 years of experience. My 55 | main stack is JavaScript-based with TypeScript, React, AWS, CDK, 56 | Cypress, and Jest, but I have also worked professionally with 57 | PHP, Python, and Java. I see languages and frameworks as tools 58 | rather than ends in themselves. 59 |

60 |

61 | I have been working as a Tech Lead since 2023 and have held 62 | lead-like responsibilities for much longer. Overall, I see 63 | myself as a 64 | force multiplier who helps 65 | teams move faster and safer. I hope we can build great things together. 66 |

67 |
68 | 69 | 70 |
71 |

74 | Professional Experience 75 |

76 | 77 | 78 |
81 |
84 |
85 |

88 | Staff Engineer 89 |

90 |

91 | Taxdoo · Hamburg, Germany · Remote 92 |

93 |
94 |

95 | January 2025 – Present 96 |

97 |
98 |
    101 |
  • 102 | Created a series of AI workflows for safe refactoring 103 | using Claude. 104 |
  • 105 |
  • 106 | Worked on a tax-based system spanning PHP, Python, Node, 107 | Go, and Java, guiding migration towards a single 108 | TypeScript and React codebase. 109 |
  • 110 |
  • 111 | Aggressively optimized GitLab pipelines with caching, 112 | conditional testing, merge trains, and parallelization, 113 | reducing optimal-path run time from ~20 minutes to ~2 114 | minutes. 115 |
  • 116 |
  • 117 | Managed AWS CDK infrastructure for both monolith and 118 | microservice architectures. 119 |
  • 120 |
  • 121 | Defined a QA testing approach for safe and fast 122 | development using Jest, Cypress, Storybook, and 123 | Chromatic. 124 |
  • 125 |
  • 126 | Mentored a senior engineer, helping them grow on their 127 | own terms, including building a Supabase-powered 128 | internal tool and migrating a legacy Java project. 129 |
  • 130 |
131 |
132 | 133 | 134 |
137 |
140 |
141 |

144 | Tech Lead 145 |

146 |

147 | Zolar (Solar energy) · Berlin, Germany · Remote 148 |

149 |
150 |

151 | February 2023 – January 2025 152 |

153 |
154 |
    157 |
  • 158 | Had 4 direct reports within a team of 6 and collaborated 159 | with over 50 stakeholders across 4 domains. 160 |
  • 161 |
  • 162 | Built systems using TypeScript, NestJS, React, CSS, 163 | Node.js, AWS, CDK, and Cypress. 164 |
  • 165 |
  • 166 | Designed end-to-end supply chain and warehouse 167 | management systems, enabling ~95% of orders to be 168 | processed without human intervention. 169 |
  • 170 |
  • 171 | Developed Salesforce and marketing integrations for 172 | seamless client communication and GDPR workflows, 173 | enabling tool migrations that reduced monthly 174 | expenditure. 175 |
  • 176 |
  • 177 | Designed integrations to connect off-the-shelf systems 178 | with third-party vendors using more than 9 protocols and 179 | communication styles. 180 |
  • 181 |
  • 182 | Improved deployment pipelines company-wide by roughly 183 | 90%. 184 |
  • 185 |
  • 186 | Implemented automated interview steps to quickly veto 187 | candidates, reducing the time to reject from ~2 weeks to 188 | ~1 day. 189 |
  • 190 |
191 |
192 | 193 | 194 |
197 |
200 |
201 |

204 | Senior Full-stack Engineer 205 |

206 |

207 | VISI/ONE (Automotive IoT) · Berlin, Germany · Remote 208 |

209 |
210 |

211 | September 2022 – February 2023 212 |

213 |
214 |
    217 |
  • 218 | Contributed to a very old, legacy JavaScript and 219 | PostgreSQL codebase. 220 |
  • 221 |
  • 222 | Played a key role in designing and executing the 223 | modernization strategy for the reformed codebase. 224 |
  • 225 |
226 |
227 | 228 | 229 |
232 |
235 |
236 |

239 | Senior Full-stack Engineer 240 |

241 |

242 | heyday (Human resources · impacted by layoffs) · 243 | Berlin, Germany · Remote 244 |

245 |
246 |

247 | October 2021 – September 2022 248 |

249 |
250 |
    253 |
  • 254 | Empowered a team of 3 strong developers working with 255 | NestJS, Next.js, Terraform, Kubernetes, AWS, Cypress, 256 | and Jest. 257 |
  • 258 |
  • 259 | Helped build a full-stack employee benefits platform. 260 |
  • 261 |
  • 262 | Built invoice and automated payment systems, reducing 263 | processes that took 3 days down to seconds. 264 |
  • 265 |
  • 266 | Developed tax automations and calculators to help 267 | employers offer optimized benefits. 268 |
  • 269 |
  • 270 | Delivered mobile-first solutions for employee benefit 271 | apps. 272 |
  • 273 |
  • 274 | Maintained Kubernetes clusters with Terraform, balancing 275 | low AWS costs with high availability. 276 |
  • 277 |
278 |
279 | 280 | 281 |
284 |
287 |
288 |

291 | Lead Backend Developer 292 |

293 |

294 | ThinxNet (now Ryd) · Automotive IoT · Munich, 295 | Germany · Remote 296 |

297 |
298 |

299 | December 2020 – September 2021 300 |

301 |
302 |
    305 |
  • 306 | Managed a team of 4 developers working on NestJS and 307 | PostgreSQL microservices. 308 |
  • 309 |
  • 310 | Implemented multiple point-of-sale integrations across 311 | varied communication protocols. 312 |
  • 313 |
  • 314 | Created a code-generation approach for integrations, 315 | cutting typical integration time from ~3 months to ~2 316 | weeks. 317 |
  • 318 |
319 |
320 | 321 | 322 |
325 |
328 |
329 |

332 | Senior Consultant 333 |

334 |

335 | Concept Reply (Automotive IoT) · Munich, Germany · 336 | Remote 337 |

338 |
339 |

340 | September 2019 – December 2020 341 |

342 |
343 |
    346 |
  • 347 | Led a rotating team of developers on industrial IoT 348 | projects. 349 |
  • 350 |
  • 351 | Worked primarily on an OPC-UA, MQTT, and 352 | TypeScript-based industrial systems management platform. 353 |
  • 354 |
  • 355 | Advised on multiple projects leveraging Python, Java, 356 | and TypeScript due to polyglot background. 357 |
  • 358 |
359 |
360 | 361 | 362 |
365 |
368 |
369 |

372 | Lead Full-stack Developer 373 |

374 |

375 | Workerbase (Industrial IoT) · Munich, Germany 376 |

377 |
378 |

379 | May 2018 – August 2019 380 |

381 |
382 |
    385 |
  • 386 | Built a full-stack app-builder for industrial processes 387 | using MQTT, Node.js, React, and OPC-UA. 388 |
  • 389 |
  • 390 | Created an end-to-end testing infrastructure using Jest, 391 | Testcontainers, and Cypress. 392 |
  • 393 |
394 |
395 | 396 | 397 |
400 |
403 |
404 |

407 | Senior Full-stack Developer 408 |

409 |

410 | Tanaza (Cloud & WiFi Management) · Milan, Italy 411 | · Remote 412 |

413 |
414 |

415 | August 2016 – May 2018 416 |

417 |
418 |
    421 |
  • 422 | Worked on WiFi management systems using React, GraphQL, 423 | PostgreSQL, TypeScript, Apollo, and Express. 424 |
  • 425 |
  • 426 | Built a corporate and public WiFi editor that was both 427 | easy to use and highly flexible. 428 |
  • 429 |
430 |
431 | 432 | 433 |
436 |
439 |
440 |

443 | Freelancer / Employed 444 |

445 |

446 | Miscellaneous · Multiple countries 447 |

448 |
449 |

450 | February 2004 – August 2016 451 |

452 |
453 |
    456 |
  • 457 | Consulted on multiple projects for an Ireland-based team 458 | augmentation company. 459 |
  • 460 |
  • 461 | Implemented an applicant tracking system that formed the 462 | basis of a Brazilian startup. 463 |
  • 464 |
  • 465 | Built multimedia projects including teaching platforms, 466 | web radios, online gambling, and remote video services. 467 |
  • 468 |
  • 469 | Served as Team Lead with 3 direct reports, handling all 470 | technology aspects of a music branding business. 471 |
  • 472 |
  • 473 | Developed a C++ DRM system allowing licensed music on 474 | disk to be played back in real time. 475 |
  • 476 |
477 |
478 |
479 | 480 | 481 |
482 |

485 | Education 486 |

487 |
490 |
493 |
494 |

497 | B.Sc. Computer Science 498 |

499 |

500 | Federal University of the ABC Region · Santo André, 501 | Brazil 502 |

503 |
504 |

505 | 2009 – 2016 506 |

507 |
508 |
509 |
512 |
515 |
516 |

519 | Technical Degree – Mechatronics 520 |

521 |

522 | Technical School Jorge Street · São Caetano do Sul, 523 | Brazil 524 |

525 |
526 |

527 | 2005 – 2006 528 |

529 |
530 |
531 |
532 | 533 | 534 |
535 |

538 | Volunteer Work 539 |

540 |
543 |

544 | ReDI School of Digital Integration 547 | – Java Teacher · Spring 2020 548 |

549 |

550 | ReDI School of Digital Integration 553 | – Java Teacher · Fall 2019 554 |

555 |

556 | freeCodeCamp São Paulo – Community Manager · 2018–2019 558 |

559 |
560 |
561 | 562 | 563 |
564 |

567 | Skills 568 |

569 |
570 |
571 |

574 | Languages 575 |

576 |

577 | Portuguese (native), English (C2), German (A2) 578 |

579 |
580 |
581 |
582 |
583 |
584 | -------------------------------------------------------------------------------- /scripts/data/allPosts.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type_of": "article", 5 | "id": 733807, 6 | "title": "Building a temporary database for testing and development", 7 | "description": "TL;DR Add the initial data to the volume, and run docker-compose with the renew anon...", 8 | "published": true, 9 | "published_at": "2021-06-20T16:32:42.369Z", 10 | "slug": "building-a-temporary-database-for-testing-and-development-2g52", 11 | "path": "/drfabio/building-a-temporary-database-for-testing-and-development-2g52", 12 | "url": "https://dev.to/drfabio/building-a-temporary-database-for-testing-and-development-2g52", 13 | "comments_count": 0, 14 | "public_reactions_count": 3, 15 | "page_views_count": 882, 16 | "published_timestamp": "2021-06-20T16:32:42Z", 17 | "body_markdown": "### TL;DR\nAdd the initial data to the volume, and run docker-compose with the renew anon volumes argument (-V)\n\n### Introduction\nSo you have built your system and now is time to test it, or you want to do a demo and everything needs to be set up on a given state. Unfortunately, you were testing some changes and now you need to clean up the database, or even worse perform a series of actions just to get it in the desired state.\n\nLuckily there are a few actions you can take to make your life easier.\n\nThese next tips will assume you have docker and docker compose installed as these will be the backbone of the solution.\n\n## Step 1 - An empty database with docker\n\nLet's say you just want a runnable database to start developing, perhaps you are playing with some ORM[^1] solution, an empty database can be quite handy.\n\nThe following docker command can do the trick for postgres:\n\n```sh\ndocker run -e POSTGRES_USER=my_user -e POSTGRES_PASSWORD=my_password -e POSTGRES_DB=some_db postgres:13 \n```\n\nA one-liner and we have an accessible database to our heart's content.\n\n## Step 2 - A databse witth initial data and docker\nA database without data is nice but a database already set up is even nicer. For that we can use [volumes](https://docs.docker.com/storage/volumes/) and an initial data for the database dump. [The initialization scripts section] of the postgres image tells us that all we need to do is copy some [dump](https://www.postgresql.org/docs/12/app-pgdump.html) to the folder /docker-entrypoint-initdb.d/.\n\nThis would be done with this argument:\n\n```sh\n--mount type=bind,source=/some/path/my_dump.sql,target=/docker-entrypoint-initdb.d/dump.sql \n```\n\nSo our full command would look like \n\n```sh\ndocker run -e POSTGRES_USER=my_user -e POSTGRES_PASSWORD=my_password -e POSTGRES_DB=some_db --mount type=bind,source=/some/path/my_dump.sql,target=/docker-entrypoint-initdb.d/dump.sql postgres:13 \n ```\n\n[^1]: [Object Relational Mapping](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping)\n\n### Step 3 - Tidying it up with docker-compose\nDocker is useful but docker-compose makes things more manageable.\n\nWith 2 tricks we can have our database running temporarily without much hassle.\n\n#### Step 3.1 Convert the docker to docker-compose yaml\n\nLet's just add the arguments of our command line to a compose file.\n\n```yaml\n# docker-compose.yml\nversion: \"3\"\nservices:\n db:\n image: postgres:13\n environment:\n POSTGRES_PASSWORD: \"my_password\"\n POSTGRES_USER: \"my_user\"\n POSTGRES_DB: \"some_db\"\n volumes:\n - ./some/path/my_dump.sql:/docker-entrypoint-initdb.d/my_dump.sql\n```\n\nThis is very nice, run it with the following command, and your database will be there same as before.\n\n```sh\ndocker-compose up\n\n```\n#### Step 3.2 Making sure our database remains the same\n\nIf we ran the previous command and delete a record we would be losing that record next time. This is sometimes not desirable for testing, demos, or even general development. I find oy very calming to have the same environment every time.\n\nTo achieve that we need to renew the volumes. Running it with the argument -V , from [compose docs](https://docs.docker.com/compose/reference/up/) ,this states the following:\n\n> -V, --renew-anon-volumes Recreate anonymous volumes instead of retrieving data from the previous containers.\n\nThis makes our command to start the database looks like this:\n\n```sh\ndocker-compose up -V db\n```\n\nNow if you delete some initial data and execute the command again the state will be brought right back to how it was.\n\nHope you folks find it helpful.\n\n\n", 18 | "positive_reactions_count": 3, 19 | "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqfv0llhii60defg3kxa.jpg", 20 | "tag_list": [ 21 | "docker", 22 | "database", 23 | "testing" 24 | ], 25 | "canonical_url": "https://dev.to/drfabio/building-a-temporary-database-for-testing-and-development-2g52", 26 | "reading_time_minutes": 3, 27 | "user": { 28 | "name": "Fabio Oliveira Costa", 29 | "username": "drfabio", 30 | "twitter_username": null, 31 | "github_username": "drFabio", 32 | "user_id": 264120, 33 | "website_url": null, 34 | "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg", 35 | "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg" 36 | } 37 | }, 38 | { 39 | "type_of": "article", 40 | "id": 292644, 41 | "title": "Introduction to container based development part 4/4 - Compose", 42 | "description": "This is the fourth part of our container based development series. Check the other posts at the end o...", 43 | "published": true, 44 | "published_at": "2020-03-26T20:15:34.841Z", 45 | "slug": "introduction-to-container-based-development-part-4-4-compose-41lp", 46 | "path": "/drfabio/introduction-to-container-based-development-part-4-4-compose-41lp", 47 | "url": "https://dev.to/drfabio/introduction-to-container-based-development-part-4-4-compose-41lp", 48 | "comments_count": 2, 49 | "public_reactions_count": 8, 50 | "page_views_count": 152, 51 | "published_timestamp": "2020-03-26T20:15:34Z", 52 | "body_markdown": "This is the fourth part of our container based development series. Check the other posts at the end of this one.\n\nThis post is based on the container development [article]( https://github.com/drFabio/containerDevelopment/blob/master/article.pdf) created by me.\n\nRunning containers manually is useful but it quickly gets out of hand when you need to manage complex architectures, to make things easy we can use [docker-compose](https://docs.docker.com/compose/). It has a nice and centralized way to create multiple containers at the same time.\n\nLet's port our old example with docker-compose and see how it looks.The [official documentation](https://docs.docker.com/compose/gettingstarted/) is full of more info and all the files for this step are on the projects repository on the [firstDockerCompose branch](https://github.com/drFabio/containerDevelopment/tree/firstDockerCompose).\n\nLet's create on the same folder that our files are a folder to use as a volume. It is customary to keep all docker-compose related files under the same root, this folder will be called \"ourLocalVolume\". Then we need to create a [compose file](https://docs.docker.com/compose/compose-file/). This compose file is defining a single service called runner, building it from a local dockerFile and creating a volume called ourLocalVolume mounted on the container /app/ourApp/data.\n\n```yml\nversion: '3'\nservices:\n runner:\n build: .\n volumes:\n - ./ourLocalVolume:/app/ourApp/data\n```\n\nTo run our solution we need to do the following command on the terminal:\n\n```shell\ndocker-compose up\n```\n\nIf we make a change to our file we need to rebuild it with:\n```shell\ndocker-compose build\n```\n\nOr we need to run and build with:\n\n```shell\ndocker-compose up --build\n```\n\nRebuilding every time is not very productive. Our code is also very simple we are not doing anything special with the node image we could easily be more productive if our code changes on the host machine would be transferred to the container. The way to achieve that is by using a volume with our code and our data , actually after all this changes we don't even need to make our own custom Dockerfile we can do everything using docker-compose. We are going to tidy our space a little so see the changes the [secondDockerCompose branch](https://github.com/drFabio/containerDevelopment/tree/secondDockerCompose).\n\nOur compose volume changed and now we are going to use the node image directly:\n```yml\nversion: '3'\nservices:\n runner:\n image: node:13-alpine3.10\n volumes:\n - ./src:/app\n working_dir: /app\n command: node ./nodeCounter.js\n```\nSince our code now lives on a volume along with our data we can update our code without needing to rebuild an image. Also since we are essentially only using a node image we do not need to create a custom image every time.\n\n## Getting dangerous, multiple services!\n\nOk compose for a single service is nice but not as nice as managing an actual architecture.\n\nWe are going to maintain this counter but it will count to up to 20 and then reset and never stop.Also we are going to make a web frontend with php that is going to show the counter as it was at the moment somebody entered the page. Our file architecture will change again so check the \n[thirdDockerCompose branch](https://github.com/drFabio/containerDevelopment/tree/thirdDockerCompose).\n\nWe are going to change our node to run from a fixed place that is going to be our volume data\n```javascript\nconst fs = require('fs')\nconst path = require('path')\n\nconst counterFile = path.resolve('/data/counter.txt')\n\nconst COUNTER_LIMIT = 20\nconst INTERVAL = 1000\nlet counterData = 0\nif (fs.existsSync(counterFile)) {\n const fileData = fs.readFileSync(counterFile)\n counterData = parseInt(fileData, 10)\n}\n\nconst stopCounter = counterData + COUNTER_LIMIT\n\nconst recursiveCounter = () => {\n if (counterData === stopCounter) {\n counterData = 0\n }\n console.log(counterData)\n counterData++\n fs.writeFileSync(counterFile, counterData)\n setTimeout(recursiveCounter, INTERVAL)\n}\n\nrecursiveCounter()\n```\n\nAnd this will be our PHP code:\n\n```php\n\n\n\n \n \n \n Counter <?=$counter ?>\n\n\n

Welcome to the counter view

\n

At this moment the counter is

\n

Reload for even more counter fun!

\n\n\n```\nOur docker-compose needs to change so it will have 2 services! Also we will link the volumes so the PHP can read what node is writing.\n\n```yml\nversion: '3'\nservices:\n runner:\n image: node:13-alpine3.10\n volumes:\n - ./src/counter:/app\n - ./src/data:/data\n working_dir: /app\n command: node ./nodeCounter.js\n web:\n image: php:7.2-apache\n volumes:\n - ./src/web:/var/www/html/\n - ./src/data:/data\n ports:\n - 8081:80\n```\n\nIf we run docker-compose up we should be able to see our page on http://localhost:8081. Reload and you will see changes.\nAlso Note that our system now never stop to exit we need to send a [SIGTERM](https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html) by typing \"ctrl+c\"\n\nLet's take a moment to appreciate what is happening:\n\n- We created 2 services based on 2 different images.\n- We created 4 volumes being 2 of them shared between different containers.\n- We create a port forwarding from a container to our host machine.\n- We \"installed\" a php with apache and node with only 16 lines of code!\n\nNow that you were introduced to containers see how they can help you be a more productive and happier dev. Also there is one bonus part on the PDF [article](https://github.com/drFabio/containerDevelopment/blob/master/article.pdf) when we add a Redis database to our Node, PHP project just for the laughs.\n\nHave fun, be happy, be healthy be kind. ", 53 | "positive_reactions_count": 8, 54 | "cover_image": null, 55 | "tag_list": [ 56 | "docker" 57 | ], 58 | "canonical_url": "https://dev.to/drfabio/introduction-to-container-based-development-part-4-4-compose-41lp", 59 | "reading_time_minutes": 4, 60 | "user": { 61 | "name": "Fabio Oliveira Costa", 62 | "username": "drfabio", 63 | "twitter_username": null, 64 | "github_username": "drFabio", 65 | "user_id": 264120, 66 | "website_url": null, 67 | "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg", 68 | "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg" 69 | } 70 | }, 71 | { 72 | "type_of": "article", 73 | "id": 292627, 74 | "title": "Introduction to container based development - Part 3/4 - Volumes", 75 | "description": "This is the third part of our container based development series This post is based on the container...", 76 | "published": true, 77 | "published_at": "2020-03-26T19:50:47.247Z", 78 | "slug": "introduction-to-container-based-development-part-3-4-volumes-3pn9", 79 | "path": "/drfabio/introduction-to-container-based-development-part-3-4-volumes-3pn9", 80 | "url": "https://dev.to/drfabio/introduction-to-container-based-development-part-3-4-volumes-3pn9", 81 | "comments_count": 0, 82 | "public_reactions_count": 12, 83 | "page_views_count": 142, 84 | "published_timestamp": "2020-03-26T19:50:47Z", 85 | "body_markdown": "This is the third part of our container based development series\n\nThis post is based on the container development [article]( https://github.com/drFabio/containerDevelopment/blob/master/article.pdf) created by me.\n\nOn the previous articles we did a program that count from 0 to 19 but could not do anything besides that. A program that can not persist is not so useful so let's modify our program a little so it will count the current file content and then count to that value +20. For that we need to create a [volume](https://docs.docker.com/storage/volumes/) on the container, this will make a specific folder on the container persist when mounted. All the files for this step are on the projects repository on the [secondContainer branch](https://github.com/drFabio/containerDevelopment/tree/secondContainer).\n\nFirst we need to add a proper volume on our docker file.\n\n**Dockefile**\n\n```Dockerfile\nFROM node:13-alpine3.10\nRUN mkdir -p /app/ourApp/data\nCOPY ./nodeCounter.js /app/ourApp/\nWORKDIR /app/ourApp/\nVOLUME /app/ourApp/data/\nCMD node ./nodeCounter.js\n```\n\nThen we need to make sure our program don't stop at 20 but stops at start + 20\n\n**nodeCounter.js**\n\n```javascript\nconst fs = require('fs')\nconst path = require('path')\n\nconst counterFile = path.resolve(__dirname, './data/counter.txt')\n\nconst COUNTER_LIMIT = 20\nconst INTERVAL = 1000\nlet counterData = 0\nif (fs.existsSync(counterFile)) {\n const fileData = fs.readFileSync(counterFile)\n counterData = parseInt(fileData, 10)\n}\n\nconst stopCounter = counterData + COUNTER_LIMIT\n\nconst recursiveCounter = () => {\n if (counterData === stopCounter) {\n process.exit(0)\n }\n console.log(counterData)\n counterData++\n fs.writeFileSync(counterFile, counterData)\n setTimeout(recursiveCounter, INTERVAL)\n}\n\nrecursiveCounter()\n```\n\nThen we need to build our image with the same name and a new tag.\n\n```shell\ndocker build -t container_dev:second .\n```\n\nWe should see an output with the text:\n\n\"Successfully tagged container_dev:second\"\n\nBut out work is not over we need to actually create a volume on our computer (the host) so our container can read and write persistent data.\n\n```shell\ndocker volume create our-named-volume\n```\nBut where physically our volume is? For that we can inspect the volume and check it's exact path.\n\n```shell\ndocker volume inspect our-named-volume\n```\nThat would give all the volume data like the one below, the mountpoint is where docker created our volume.\n\n```text\n[\n {\n \"CreatedAt\": \"2020-03-20T10:44:48+01:00\",\n \"Driver\": \"local\",\n \"Labels\": {},\n \"Mountpoint\": \"/var/lib/docker/volumes/our-named-volume/_data\",\n \"Name\": \"our-named-volume\",\n \"Options\": {},\n \"Scope\": \"local\"\n }\n]\n```\n\nNow to run our app with one new argument , -v to link the volume\n\n```shell\ndocker run -v our-named-volume:/app/ourApp container_dev:second\n```\n\nRun it twice and you will notice we start where we stopped. If you open 2 shells and do it a little bit after the first one start you should see they are sharing and modifying the same file (There is a race condition here but we don't mind for now).\n\nWe have a way to persist state, actually we even have a way to share the same set of files between multiple containers!\n\nI would like to call attention about the importance of mounting, if we run without the \"-v\" command we will have the same behavior as the first time, our state would not persist.\n\nSo let's see what we learned\n\n - For a volume to be accessible in the container we need to create it at some location inside the container.\n - We need to link a volume with the -v when running the docker run argument.\n - Multiple containers can have access to the same volume.\n\nNext part: Containers and installing PHP alongside node in 16 lines of code!\n\n\n\n", 86 | "positive_reactions_count": 12, 87 | "cover_image": null, 88 | "tag_list": [ 89 | "docker" 90 | ], 91 | "canonical_url": "https://dev.to/drfabio/introduction-to-container-based-development-part-3-4-volumes-3pn9", 92 | "reading_time_minutes": 3, 93 | "user": { 94 | "name": "Fabio Oliveira Costa", 95 | "username": "drfabio", 96 | "twitter_username": null, 97 | "github_username": "drFabio", 98 | "user_id": 264120, 99 | "website_url": null, 100 | "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg", 101 | "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg" 102 | } 103 | }, 104 | { 105 | "type_of": "article", 106 | "id": 292615, 107 | "title": "Introduction to container based development - Part 2/4", 108 | "description": "This is the second part of our container based development series. This post is based on the containe...", 109 | "published": true, 110 | "published_at": "2020-03-26T19:25:22.347Z", 111 | "slug": "introduction-to-container-based-development-part-2-4-1205", 112 | "path": "/drfabio/introduction-to-container-based-development-part-2-4-1205", 113 | "url": "https://dev.to/drfabio/introduction-to-container-based-development-part-2-4-1205", 114 | "comments_count": 0, 115 | "public_reactions_count": 10, 116 | "page_views_count": 71, 117 | "published_timestamp": "2020-03-26T19:25:22Z", 118 | "body_markdown": "This is the second part of our container based development series. This post is based on the container development [article]( https://github.com/drFabio/containerDevelopment/blob/master/article.pdf) created by me.\n\n\n### Creating our image - Pressing our own CD.\n\nWe are going create a node program its behavior is the following.\n\n \n- try to find a file and read from it;\n- output the content of the file;\n- save the content +1;\n- repeat every 1 second until 19.\n- if no content is found it will create the file and save with 0;\n\nThe code is on the repository on the [firstContainer branch](https://github.com/drFabio/containerDevelopment/tree/firstContainer).\n\n**nodeCounter.js**\n```javascript\nconst fs = require('fs')\nconst path = require('path')\n\nconst counterFile = path.resolve(__dirname, './counter.txt')\n\nconst COUNTER_LIMIT = 20\nconst INTERVAL = 1000\nlet counterData = 0\nif (fs.existsSync(counterFile)) {\n const fileData = fs.readFileSync(counterFile)\n counterData = parseInt(fileData, 10)\n}\n\nconst recursiveCounter = () => {\n if (counterData === COUNTER_LIMIT) {\n process.exit(0)\n }\n console.log(counterData)\n counterData++\n fs.writeFileSync(counterFile, counterData)\n setTimeout(recursiveCounter, INTERVAL)\n}\n\nrecursiveCounter()\n```\n\nWith this code on some folder we also need to configure our container, to make how our CD is going to be built.\n\n*Dockerfile* (No extension)\n```dockerfile\nFROM node:13-alpine3.10\nRUN mkdir -p /app/ourApp/\nCOPY ./nodeCounter.js /app/ourApp/\nWORKDIR /app/ourApp/\nCMD node ./nodeCounter.js\n```\n\nThe dockerfile above have [some commands](https://docs.docker.com/engine/reference/builder/)\n\n* [FROM](https://docs.docker.com/engine/reference/builder/#from) - We are using a Node image that uses the alpien linux operating system.\n* [RUN](https://docs.docker.com/engine/reference/builder/#run) - On this image we are creating a folder.\n* [COPY](https://docs.docker.com/engine/reference/builder/#copy) - We are copying our node program to the folder.\n* [WORKDIR](https://docs.docker.com/engine/reference/builder/#workdir) - We change the working dir so all commands will run starting from this new location.\n* [CMD](https://docs.docker.com/engine/reference/builder/#cmd) - We say which command should run as soon as you play the CD.\n\nTo make our cd we execute the following on the terminal, it says to build a image called container_dev with the tag first using the dockerfile on the same folder I am executing the command.\n\n```shell\ndocker build -t container_dev:first .\n```\nThe output will be something like:\n\n```text\nSending build context to Docker daemon 70.66kB\nStep 1/5 : FROM node:13-alpine3.10\n.......\nSuccessfully built 50c9fae3a235\nSuccessfully tagged container_dev:first\n```\nIf we want to see our images we can type \n\n```shell\ndocker images\n```\n\nTo see our container data like Ids, names , tags and so on.\n\n```text\nREPOSITORY TAG IMAGE ID\ncontainer_dev first 50c9fae3a235 \nnode 13-alpine3.10 \n```\n\nNow that our image has a name and a tag we can run it:\n\n```shell\ndocker run container_dev:first\n```\n\nAnd you should see a count from 0 to 19 and our image stopping. Congratulations you just created your first image!\nThis image is saving data on a file, run it again if we persisted data it should stop immediately since we are already on the 20 but now it will start again from 0, run it twice with a little delay each count will not interfere with each other. That is part of the security of containers they are isolated.\n\nOur next topic: Volumes!\n\n", 119 | "positive_reactions_count": 10, 120 | "cover_image": null, 121 | "tag_list": [ 122 | "docker" 123 | ], 124 | "canonical_url": "https://dev.to/drfabio/introduction-to-container-based-development-part-2-4-1205", 125 | "reading_time_minutes": 2, 126 | "user": { 127 | "name": "Fabio Oliveira Costa", 128 | "username": "drfabio", 129 | "twitter_username": null, 130 | "github_username": "drFabio", 131 | "user_id": 264120, 132 | "website_url": null, 133 | "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg", 134 | "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg" 135 | } 136 | }, 137 | { 138 | "type_of": "article", 139 | "id": 292560, 140 | "title": "Introduction to container based development - Part 1/4", 141 | "description": "This post is based on the container development article wrote by me. We are going to review some basi...", 142 | "published": true, 143 | "published_at": "2020-03-26T19:19:53.719Z", 144 | "slug": "introduction-to-container-based-development-part-1-4-1ai5", 145 | "path": "/drfabio/introduction-to-container-based-development-part-1-4-1ai5", 146 | "url": "https://dev.to/drfabio/introduction-to-container-based-development-part-1-4-1ai5", 147 | "comments_count": 0, 148 | "public_reactions_count": 12, 149 | "page_views_count": 277, 150 | "published_timestamp": "2020-03-26T19:19:53Z", 151 | "body_markdown": "This post is based on the container development [article]( https://github.com/drFabio/containerDevelopment/blob/master/article.pdf) wrote by me. We are going to review some basics about containers and hopefully you are going to start loving and using them as much as me.\n\n\n## Requirements\nHave [docker](https://docs.docker.com/install/) and [docker-compose]({https://docs.docker.com/compose/install/) installed.\n\n## Introduction \nContainers borrow the name from shipping containers a mean of transportation that revolutionized the way we ship goods. Be it 100 tons of tuna, a whole house or thousands of unrelated Amazon packages they can be easily transported from anywhere to anywhere. Software containers are the same, the handlers of the containers don't care about what is inside they know how to handle containers and that is what make them flexible.\n\nOn the software world developing with containers allow us to eliminate the \"it works on my machine\", run multiple versions of the same software, create complex architectures on a local machine, make micros services with hundreds of different languages and many more nice things.\n\n## The concepts\n\n### Image\n\nThink of an image as a read only data, like a music “CD”. You could play the PHP CD, perhaps you are feeling a little javazzy and play the java CD or you want to do your own thing so you remix a CD and put the data that you need. The CD *does not have a state* it is what it is and that particular CD will always have the same contents.\nA image is built with layers on top of a filesystem. Usually you create an image using another image and create it with a series of instructions, if docker already has these instructions on cache it will use that cache instead of building from scratch. Still on our CD metaphor the CD is done with each track separated, you have the drums, the guitar, the bass and the vocals each on a different layer, if you had a karaoke version of the CD the studio one would be the karaoke with the vocals track on top of it.\n\nImages are listed on a repository like [docker hub](https://hub.docker.com/) and have a name and a tag.\n\n![An image with its layers](https://dev-to-uploads.s3.amazonaws.com/i/ne7bk9n3pcs1lif95omj.png)\n\n### Container\n\nA container is a running image and *it has state but does not have persistence*. Think about a CD and somebody manipulating it like a DJ, it is doing changes to the current state of the CD but by the time the execution is done the CD is back on the case and nothing changed on it. The execution of the CD was the container and it can change as much as we need but it's changes will not change the image itself.\n\n### Volumes\n\nA volume is where we have *persistent state*, this is where our changes are not lost, this is how containers read and write persistent data. For example think on an application that counts sales, it would be a pretty lousy application if we lost all sale data as soon as the application was shutdown, for that we persist data on volumes. Most database images will have a volume. Still on the musical analogy our volume is our live recording, we are working on it and it will change, even if we have some fixed tracks we are still changing the data on this one.\n\n\n| |State|Persistence|\n|---|---|---|\n|Image| No | No |\n|Container| Yes | No |\n|Volume| No | Yes |\n\n## Getting started\n\n### Running the first image - Playing the CD\nFirst we need our image on our system so we pull it from the repository. Execute the code below on you terminal.\n\n```shell\ndocker pull docker/whalesay:latest\n```\nThe output should say something like:\n\n\"Status: Downloaded newer image for docker/whalesay:latest\"\n\nThe image is on our system now we need to run it (Play the cd):\n\n```shell\ndocker run docker/whalesay cowsay \"Docker is awesome\"\n```\n\nAnd you should see the following:\n\n```text\n< Docker is awesome >\n ------------------- \n \\\n \\\n \\ \n ## . \n ## ## ## == \n ## ## ## ## === \n /\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"___/ === \n ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ \n \\______ o __/ \n \\ \\ __/ \n \\____\\______/ \n```\n\nSo what happened? For a brief moment we run our image, that generated a container that printed a whale on the screen and then the container stopped.\n\nCongratulations you just used your first image and created your first container!\n\n### Acknowledgement\n\n[Photo by Kaique Rocha from Pexels](https://www.pexels.com/photo/business-commerce-container-export-379964/)", 152 | "positive_reactions_count": 12, 153 | "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9bcgf9cbctedu3pejycz.jpg", 154 | "tag_list": [ 155 | "docker" 156 | ], 157 | "canonical_url": "https://dev.to/drfabio/introduction-to-container-based-development-part-1-4-1ai5", 158 | "reading_time_minutes": 3, 159 | "user": { 160 | "name": "Fabio Oliveira Costa", 161 | "username": "drfabio", 162 | "twitter_username": null, 163 | "github_username": "drFabio", 164 | "user_id": 264120, 165 | "website_url": null, 166 | "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg", 167 | "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F264120%2F32b420f3-5d92-4e5a-9079-5c1961b38833.jpeg" 168 | } 169 | } 170 | ] 171 | } --------------------------------------------------------------------------------