[{"data":1,"prerenderedAt":1575},["ShallowReactive",2],{"i-lucide:languages":3,"i-lucide:chevron-down":8,"i-lucide:moon":10,"i-lucide:menu":12,"i-lucide:mail":14,"i-lucide:github":16,"i-lucide:linkedin":19,"blog-\u002Fen\u002Fblog\u002Fdockerize-nestjs":21,"services-all-en":997,"i-lucide:arrow-left":1571,"i-lucide:code-xml":1573},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m5 8l6 6m-7 0l6-6l2-3M2 5h12M7 2h1m14 20l-5-10l-5 10m2-4h6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 9l6 6l6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"\u002F>\u003Crect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":17,"hidden":18},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5c.08-1.25-.27-2.48-1-3.5c.28-1.15.28-2.35 0-3.5c0 0-1 0-3 1.5c-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5c-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4\"\u002F>\u003Cpath d=\"M9 18c-4.51 2-5-2-7-2\"\u002F>\u003C\u002Fg>",true,{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":20,"hidden":18},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2a2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6M2 9h4v12H2z\"\u002F>\u003Ccircle cx=\"4\" cy=\"4\" r=\"2\"\u002F>\u003C\u002Fg>",{"id":22,"title":23,"body":24,"category":969,"date":970,"description":971,"draft":6,"extension":972,"faq":973,"image":986,"meta":987,"navigation":18,"path":988,"seo":989,"stem":990,"tags":991,"updated":995,"__hash__":996},"blog_en\u002Fen\u002Fblog\u002Fdockerize-nestjs.md","Dockerizing a NestJS Application: The Complete Guide",{"type":25,"value":26,"toc":959},"minimark",[27,50,55,58,80,83,87,90,283,286,306,312,316,334,342,349,353,360,610,621,625,628,828,835,839,892,896,911,915,943,955],[28,29,30,31,35,36,43,44,49],"p",{},"In my freelance engagements, I systematically containerize applications. Here is the method I apply for a ",[32,33,34],"strong",{},"NestJS"," API, with an optimized build and reliable deployment. This approach follows ",[37,38,42],"a",{"href":39,"rel":40},"https:\u002F\u002Fdocs.docker.com\u002Fbuild\u002Fbuilding\u002Fmulti-stage\u002F",[41],"nofollow","Docker's official multi-stage build recommendations"," and the ",[37,45,48],{"href":46,"rel":47},"https:\u002F\u002Fdocs.nestjs.com\u002F",[41],"NestJS documentation",".",[51,52,54],"h2",{"id":53},"why-containerize-a-nestjs-api","Why containerize a NestJS API?",[28,56,57],{},"Before the how, the why. Dockerizing a NestJS API brings three concrete benefits:",[59,60,61,68,74],"ul",{},[62,63,64,67],"li",{},[32,65,66],{},"Environmental parity",": the image running in production is exactly the one tested locally. No more \"but it worked on my machine\".",[62,69,70,73],{},[32,71,72],{},"Reproducible deployments",": same Node version, same dependencies, same configuration variables — on any server.",[62,75,76,79],{},[32,77,78],{},"Isolation",": each API runs in its own container with its own resources. A leaky neighbouring application doesn't impact yours.",[28,81,82],{},"For an SMB managing several applications, it's the move from a \"tinkered-together\" server to infrastructure documented as code.",[51,84,86],{"id":85},"the-multi-stage-dockerfile","The multi-stage Dockerfile",[28,88,89],{},"The idea: separate the build from the runtime to drastically reduce the final image size.",[91,92,97],"pre",{"className":93,"code":94,"language":95,"meta":96,"style":96},"language-dockerfile shiki shiki-themes github-dark github-dark","# Stage 1 — build\nFROM node:22-alpine AS builder\nWORKDIR \u002Fapp\nCOPY package*.json .\u002F\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# Stage 2 — production\nFROM node:22-alpine AS runner\nWORKDIR \u002Fapp\nENV NODE_ENV=production\nCOPY package*.json .\u002F\nRUN npm ci --omit=dev && npm cache clean --force\nCOPY --from=builder \u002Fapp\u002Fdist .\u002Fdist\n\n# Security: don't run as root\nUSER node\nEXPOSE 3000\nCMD [\"node\", \"dist\u002Fmain.js\"]\n","dockerfile","",[98,99,100,109,126,135,144,153,161,169,175,181,193,200,209,216,224,232,237,243,252,261],"code",{"__ignoreMap":96},[101,102,105],"span",{"class":103,"line":104},"line",1,[101,106,108],{"class":107},"sJ8bj","# Stage 1 — build\n",[101,110,112,116,120,123],{"class":103,"line":111},2,[101,113,115],{"class":114},"sOPea","FROM",[101,117,119],{"class":118},"suv1-"," node:22-alpine ",[101,121,122],{"class":114},"AS",[101,124,125],{"class":118}," builder\n",[101,127,129,132],{"class":103,"line":128},3,[101,130,131],{"class":114},"WORKDIR",[101,133,134],{"class":118}," \u002Fapp\n",[101,136,138,141],{"class":103,"line":137},4,[101,139,140],{"class":114},"COPY",[101,142,143],{"class":118}," package*.json .\u002F\n",[101,145,147,150],{"class":103,"line":146},5,[101,148,149],{"class":114},"RUN",[101,151,152],{"class":118}," npm ci\n",[101,154,156,158],{"class":103,"line":155},6,[101,157,140],{"class":114},[101,159,160],{"class":118}," . .\n",[101,162,164,166],{"class":103,"line":163},7,[101,165,149],{"class":114},[101,167,168],{"class":118}," npm run build\n",[101,170,172],{"class":103,"line":171},8,[101,173,174],{"emptyLinePlaceholder":18},"\n",[101,176,178],{"class":103,"line":177},9,[101,179,180],{"class":107},"# Stage 2 — production\n",[101,182,184,186,188,190],{"class":103,"line":183},10,[101,185,115],{"class":114},[101,187,119],{"class":118},[101,189,122],{"class":114},[101,191,192],{"class":118}," runner\n",[101,194,196,198],{"class":103,"line":195},11,[101,197,131],{"class":114},[101,199,134],{"class":118},[101,201,203,206],{"class":103,"line":202},12,[101,204,205],{"class":114},"ENV",[101,207,208],{"class":118}," NODE_ENV=production\n",[101,210,212,214],{"class":103,"line":211},13,[101,213,140],{"class":114},[101,215,143],{"class":118},[101,217,219,221],{"class":103,"line":218},14,[101,220,149],{"class":114},[101,222,223],{"class":118}," npm ci --omit=dev && npm cache clean --force\n",[101,225,227,229],{"class":103,"line":226},15,[101,228,140],{"class":114},[101,230,231],{"class":118}," --from=builder \u002Fapp\u002Fdist .\u002Fdist\n",[101,233,235],{"class":103,"line":234},16,[101,236,174],{"emptyLinePlaceholder":18},[101,238,240],{"class":103,"line":239},17,[101,241,242],{"class":107},"# Security: don't run as root\n",[101,244,246,249],{"class":103,"line":245},18,[101,247,248],{"class":114},"USER",[101,250,251],{"class":118}," node\n",[101,253,255,258],{"class":103,"line":254},19,[101,256,257],{"class":114},"EXPOSE",[101,259,260],{"class":118}," 3000\n",[101,262,264,267,270,274,277,280],{"class":103,"line":263},20,[101,265,266],{"class":114},"CMD",[101,268,269],{"class":118}," [",[101,271,273],{"class":272},"s4wv1","\"node\"",[101,275,276],{"class":118},", ",[101,278,279],{"class":272},"\"dist\u002Fmain.js\"",[101,281,282],{"class":118},"]\n",[28,284,285],{},"Two details make all the difference compared to a naive Dockerfile:",[287,288,289,298],"ol",{},[62,290,291,297],{},[32,292,293,296],{},[98,294,295],{},"npm ci --omit=dev"," in the production stage",": only runtime dependencies get reinstalled. TypeScript, ESLint and other dev tools stay in the build stage, which is discarded.",[62,299,300,305],{},[32,301,302],{},[98,303,304],{},"USER node",": the process doesn't run as root inside the container.",[28,307,308,309,49],{},"Result: an image that goes from ~1.2 GB down to ",[32,310,311],{},"~180 MB",[51,313,315],{"id":314},"the-dockerignore-forgotten-yet-essential","The .dockerignore, forgotten yet essential",[28,317,318,319,322,323,326,327,276,330,333],{},"Without a ",[98,320,321],{},".dockerignore",", the ",[98,324,325],{},"COPY . ."," includes ",[98,328,329],{},"node_modules",[98,331,332],{},".git",", tests and local files in the build context — bloated images and slow builds:",[91,335,340],{"className":336,"code":338,"language":339},[337],"language-text","node_modules\ndist\n.git\n.gitignore\n.env*\n*.md\ntest\ncoverage\n.dockerignore\nDockerfile\n","text",[98,341,338],{"__ignoreMap":96},[28,343,344,345,348],{},"Security bonus: if your ",[98,346,347],{},".env"," (with your secrets) never enters the build context, it can't end up in an image layer.",[51,350,352],{"id":351},"orchestrating-with-docker-compose","Orchestrating with docker-compose",[28,354,355,356,359],{},"In production as in development, a ",[98,357,358],{},"docker-compose.yml"," describes the whole stack. Example with PostgreSQL:",[91,361,365],{"className":362,"code":363,"language":364,"meta":96,"style":96},"language-yaml shiki shiki-themes github-dark github-dark","services:\n  api:\n    build: .\n    ports:\n      - \"3000:3000\"\n    environment:\n      DATABASE_URL: postgresql:\u002F\u002Fapp:secret@db:5432\u002Fapp\n    depends_on:\n      db:\n        condition: service_healthy\n    restart: unless-stopped\n\n  db:\n    image: postgres:17-alpine\n    environment:\n      POSTGRES_USER: app\n      POSTGRES_PASSWORD: secret\n      POSTGRES_DB: app\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U app\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\nvolumes:\n  pgdata:\n","yaml",[98,366,367,376,383,395,402,410,417,427,434,441,451,461,465,472,482,488,498,508,517,524,531,539,558,569,578,589,594,602],{"__ignoreMap":96},[101,368,369,373],{"class":103,"line":104},[101,370,372],{"class":371},"sxg3X","services",[101,374,375],{"class":118},":\n",[101,377,378,381],{"class":103,"line":111},[101,379,380],{"class":371},"  api",[101,382,375],{"class":118},[101,384,385,388,391],{"class":103,"line":128},[101,386,387],{"class":371},"    build",[101,389,390],{"class":118},": ",[101,392,394],{"class":393},"s8ozJ",".\n",[101,396,397,400],{"class":103,"line":137},[101,398,399],{"class":371},"    ports",[101,401,375],{"class":118},[101,403,404,407],{"class":103,"line":146},[101,405,406],{"class":118},"      - ",[101,408,409],{"class":272},"\"3000:3000\"\n",[101,411,412,415],{"class":103,"line":155},[101,413,414],{"class":371},"    environment",[101,416,375],{"class":118},[101,418,419,422,424],{"class":103,"line":163},[101,420,421],{"class":371},"      DATABASE_URL",[101,423,390],{"class":118},[101,425,426],{"class":272},"postgresql:\u002F\u002Fapp:secret@db:5432\u002Fapp\n",[101,428,429,432],{"class":103,"line":171},[101,430,431],{"class":371},"    depends_on",[101,433,375],{"class":118},[101,435,436,439],{"class":103,"line":177},[101,437,438],{"class":371},"      db",[101,440,375],{"class":118},[101,442,443,446,448],{"class":103,"line":183},[101,444,445],{"class":371},"        condition",[101,447,390],{"class":118},[101,449,450],{"class":272},"service_healthy\n",[101,452,453,456,458],{"class":103,"line":195},[101,454,455],{"class":371},"    restart",[101,457,390],{"class":118},[101,459,460],{"class":272},"unless-stopped\n",[101,462,463],{"class":103,"line":202},[101,464,174],{"emptyLinePlaceholder":18},[101,466,467,470],{"class":103,"line":211},[101,468,469],{"class":371},"  db",[101,471,375],{"class":118},[101,473,474,477,479],{"class":103,"line":218},[101,475,476],{"class":371},"    image",[101,478,390],{"class":118},[101,480,481],{"class":272},"postgres:17-alpine\n",[101,483,484,486],{"class":103,"line":226},[101,485,414],{"class":371},[101,487,375],{"class":118},[101,489,490,493,495],{"class":103,"line":234},[101,491,492],{"class":371},"      POSTGRES_USER",[101,494,390],{"class":118},[101,496,497],{"class":272},"app\n",[101,499,500,503,505],{"class":103,"line":239},[101,501,502],{"class":371},"      POSTGRES_PASSWORD",[101,504,390],{"class":118},[101,506,507],{"class":272},"secret\n",[101,509,510,513,515],{"class":103,"line":245},[101,511,512],{"class":371},"      POSTGRES_DB",[101,514,390],{"class":118},[101,516,497],{"class":272},[101,518,519,522],{"class":103,"line":254},[101,520,521],{"class":371},"    volumes",[101,523,375],{"class":118},[101,525,526,528],{"class":103,"line":263},[101,527,406],{"class":118},[101,529,530],{"class":272},"pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n",[101,532,534,537],{"class":103,"line":533},21,[101,535,536],{"class":371},"    healthcheck",[101,538,375],{"class":118},[101,540,542,545,548,551,553,556],{"class":103,"line":541},22,[101,543,544],{"class":371},"      test",[101,546,547],{"class":118},": [",[101,549,550],{"class":272},"\"CMD-SHELL\"",[101,552,276],{"class":118},[101,554,555],{"class":272},"\"pg_isready -U app\"",[101,557,282],{"class":118},[101,559,561,564,566],{"class":103,"line":560},23,[101,562,563],{"class":371},"      interval",[101,565,390],{"class":118},[101,567,568],{"class":272},"5s\n",[101,570,571,574,576],{"class":103,"line":5},[101,572,573],{"class":371},"      timeout",[101,575,390],{"class":118},[101,577,568],{"class":272},[101,579,581,584,586],{"class":103,"line":580},25,[101,582,583],{"class":371},"      retries",[101,585,390],{"class":118},[101,587,588],{"class":393},"5\n",[101,590,592],{"class":103,"line":591},26,[101,593,174],{"emptyLinePlaceholder":18},[101,595,597,600],{"class":103,"line":596},27,[101,598,599],{"class":371},"volumes",[101,601,375],{"class":118},[101,603,605,608],{"class":103,"line":604},28,[101,606,607],{"class":371},"  pgdata",[101,609,375],{"class":118},[28,611,612,613,616,617,620],{},"The key point: ",[98,614,615],{},"depends_on"," with ",[98,618,619],{},"condition: service_healthy",". Without a healthcheck on the database, the API starts before PostgreSQL and crashes on connection — a classic.",[51,622,624],{"id":623},"cicd-build-and-push-automatically","CI\u002FCD: build and push automatically",[28,626,627],{},"With GitHub Actions, every tag pushes the image to your registry:",[91,629,631],{"className":362,"code":630,"language":364,"meta":96,"style":96},"name: Build & Push\non:\n  push:\n    tags: ['v*']\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: docker\u002Flogin-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - uses: docker\u002Fbuild-push-action@v6\n        with:\n          push: true\n          tags: ghcr.io\u002F${{ github.repository }}:${{ github.ref_name }}\n",[98,632,633,643,650,657,669,673,680,687,697,704,714,724,731,743,754,761,771,781,791,802,808,818],{"__ignoreMap":96},[101,634,635,638,640],{"class":103,"line":104},[101,636,637],{"class":371},"name",[101,639,390],{"class":118},[101,641,642],{"class":272},"Build & Push\n",[101,644,645,648],{"class":103,"line":111},[101,646,647],{"class":393},"on",[101,649,375],{"class":118},[101,651,652,655],{"class":103,"line":128},[101,653,654],{"class":371},"  push",[101,656,375],{"class":118},[101,658,659,662,664,667],{"class":103,"line":137},[101,660,661],{"class":371},"    tags",[101,663,547],{"class":118},[101,665,666],{"class":272},"'v*'",[101,668,282],{"class":118},[101,670,671],{"class":103,"line":146},[101,672,174],{"emptyLinePlaceholder":18},[101,674,675,678],{"class":103,"line":155},[101,676,677],{"class":371},"jobs",[101,679,375],{"class":118},[101,681,682,685],{"class":103,"line":163},[101,683,684],{"class":371},"  docker",[101,686,375],{"class":118},[101,688,689,692,694],{"class":103,"line":171},[101,690,691],{"class":371},"    runs-on",[101,693,390],{"class":118},[101,695,696],{"class":272},"ubuntu-latest\n",[101,698,699,702],{"class":103,"line":177},[101,700,701],{"class":371},"    permissions",[101,703,375],{"class":118},[101,705,706,709,711],{"class":103,"line":183},[101,707,708],{"class":371},"      contents",[101,710,390],{"class":118},[101,712,713],{"class":272},"read\n",[101,715,716,719,721],{"class":103,"line":195},[101,717,718],{"class":371},"      packages",[101,720,390],{"class":118},[101,722,723],{"class":272},"write\n",[101,725,726,729],{"class":103,"line":202},[101,727,728],{"class":371},"    steps",[101,730,375],{"class":118},[101,732,733,735,738,740],{"class":103,"line":211},[101,734,406],{"class":118},[101,736,737],{"class":371},"uses",[101,739,390],{"class":118},[101,741,742],{"class":272},"actions\u002Fcheckout@v4\n",[101,744,745,747,749,751],{"class":103,"line":218},[101,746,406],{"class":118},[101,748,737],{"class":371},[101,750,390],{"class":118},[101,752,753],{"class":272},"docker\u002Flogin-action@v3\n",[101,755,756,759],{"class":103,"line":226},[101,757,758],{"class":371},"        with",[101,760,375],{"class":118},[101,762,763,766,768],{"class":103,"line":234},[101,764,765],{"class":371},"          registry",[101,767,390],{"class":118},[101,769,770],{"class":272},"ghcr.io\n",[101,772,773,776,778],{"class":103,"line":239},[101,774,775],{"class":371},"          username",[101,777,390],{"class":118},[101,779,780],{"class":272},"${{ github.actor }}\n",[101,782,783,786,788],{"class":103,"line":245},[101,784,785],{"class":371},"          password",[101,787,390],{"class":118},[101,789,790],{"class":272},"${{ secrets.GITHUB_TOKEN }}\n",[101,792,793,795,797,799],{"class":103,"line":254},[101,794,406],{"class":118},[101,796,737],{"class":371},[101,798,390],{"class":118},[101,800,801],{"class":272},"docker\u002Fbuild-push-action@v6\n",[101,803,804,806],{"class":103,"line":263},[101,805,758],{"class":371},[101,807,375],{"class":118},[101,809,810,813,815],{"class":103,"line":533},[101,811,812],{"class":371},"          push",[101,814,390],{"class":118},[101,816,817],{"class":393},"true\n",[101,819,820,823,825],{"class":103,"line":541},[101,821,822],{"class":371},"          tags",[101,824,390],{"class":118},[101,826,827],{"class":272},"ghcr.io\u002F${{ github.repository }}:${{ github.ref_name }}\n",[28,829,830,831,834],{},"On the server, a simple ",[98,832,833],{},"docker pull && docker compose up -d"," (often automated via watchtower or a webhook) is enough to deploy.",[51,836,838],{"id":837},"best-practices","Best practices",[59,840,841,855,868,874,886],{},[62,842,843,846,847,850,851,854],{},[32,844,845],{},"Always pin versions"," (",[98,848,849],{},"node:22-alpine",", not ",[98,852,853],{},"latest",").",[62,856,857,862,863,276,865,867],{},[32,858,859,860],{},"Use a ",[98,861,321],{}," to exclude ",[98,864,329],{},[98,866,332],{},", tests.",[62,869,870,873],{},[32,871,872],{},"Pass secrets via environment variables",", never inside the image.",[62,875,876,846,879,882,883,49],{},[32,877,878],{},"Listen for shutdown signals",[98,880,881],{},"SIGTERM",") for graceful stops: NestJS handles this natively via ",[98,884,885],{},"app.enableShutdownHooks()",[62,887,888,891],{},[32,889,890],{},"Combine with CI\u002FCD"," (GitHub Actions) to build and push the image automatically.",[51,893,895],{"id":894},"going-further","Going further",[28,897,898,899,846,902,907,908,910],{},"For production, I usually add a ",[32,900,901],{},"Caddy reverse proxy",[37,903,906],{"href":904,"rel":905},"https:\u002F\u002Fcaddyserver.com\u002Fdocs\u002Fautomatic-https",[41],"automatic HTTPS via Let's Encrypt",") in front of the container, and a ",[98,909,358],{}," to orchestrate the API + the PostgreSQL database.",[51,912,914],{"id":913},"official-sources","Official sources",[59,916,917,923,930,936],{},[62,918,919],{},[37,920,922],{"href":39,"rel":921},[41],"Docker — multi-stage builds",[62,924,925],{},[37,926,929],{"href":927,"rel":928},"https:\u002F\u002Fdocs.docker.com\u002Fdevelop\u002Fdevelop-images\u002Fdockerfile_best-practices\u002F",[41],"Docker — build best practices",[62,931,932],{},[37,933,935],{"href":46,"rel":934},[41],"NestJS — official documentation",[62,937,938],{},[37,939,942],{"href":940,"rel":941},"https:\u002F\u002Fgithub.com\u002Fnodejs\u002Fdocker-node\u002Fblob\u002Fmain\u002Fdocs\u002FBestPractices.md",[41],"Node.js — official Docker image",[28,944,945,946,950,951,49],{},"Want to industrialize your deployments? ",[37,947,949],{"href":948},"\u002Fen\u002F#contact","Let's talk"," or ",[37,952,954],{"href":953},"\u002Fen\u002Fdevis","estimate your DevOps engagement",[956,957,958],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sxg3X, html code.shiki .sxg3X{--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":96,"searchDepth":128,"depth":128,"links":960},[961,962,963,964,965,966,967,968],{"id":53,"depth":111,"text":54},{"id":85,"depth":111,"text":86},{"id":314,"depth":111,"text":315},{"id":351,"depth":111,"text":352},{"id":623,"depth":111,"text":624},{"id":837,"depth":111,"text":838},{"id":894,"depth":111,"text":895},{"id":913,"depth":111,"text":914},"DevOps","2025-05-22","From multi-stage Dockerfile to secure production: the complete method I use to containerize a NestJS API — compose, healthchecks, CI\u002FCD and reverse proxy.","md",[974,977,980,983],{"question":975,"answer":976},"How big is an optimized NestJS Docker image?","With a multi-stage build and production-only dependencies (`npm ci --omit=dev`), a NestJS image drops from ~1.2 GB to 180-250 MB. Using node:22-alpine as the base and pruning devDependencies explains most of the gain.",{"question":978,"answer":979},"Should the Node container run as root?","No. By default containers run as root, which widens the attack surface. Create a dedicated user in the Dockerfile (`USER node`): if compromised, the attacker doesn't get root privileges inside the container.",{"question":981,"answer":982},"How do you pass secrets (database, JWT) to a container?","Always via environment variables (non-versioned .env file, Docker secrets, or your CI\u002FCD platform's variables). Never inside the image: anything copied into a layer is recoverable by anyone with access to the image.",{"question":984,"answer":985},"Can you dockerize a NestJS API using Prisma or TypeORM?","Yes, with no fundamental difference. The only subtlety: client generation (prisma generate) must happen in the build stage, and migrations run at startup or via a dedicated job — not on every container creation.",null,{},"\u002Fen\u002Fblog\u002Fdockerize-nestjs",{"title":23,"description":971},"en\u002Fblog\u002Fdockerize-nestjs",[992,34,993,969,994],"Docker","Node.js","CI\u002FCD","2026-09-07","kT0iRRLkFhhO_kz0dDQq90j6cRoNyBuTuOwx9JIBPLo",[998,1445],{"id":999,"title":1000,"body":1001,"description":1407,"extension":972,"faq":1408,"h1":1433,"icon":1434,"keywords":1435,"meta":1440,"navigation":18,"path":1441,"seo":1442,"stem":1443,"__hash__":1444},"services_en\u002Fen\u002Fservices\u002Fdeveloppement-nuxt.md","Nuxt Development — Expert Vue.js & Nuxt 4 Freelance Developer",{"type":25,"value":1002,"toc":1397},[1003,1010,1014,1017,1053,1057,1064,1078,1081,1085,1092,1099,1125,1128,1132,1135,1155,1158,1162,1165,1253,1260,1266,1270,1273,1334,1341,1345,1348,1380,1383,1387],[28,1004,1005,1006,1009],{},"The ",[32,1007,1008],{},"Nuxt framework"," (built on Vue.js) has been my go-to technology for years: it's what I use to build fast, well-ranked and maintainable web applications — including this very website. As a freelance developer specialised in Nuxt, here is concretely how I can help you, at what cost, and with which method.",[51,1011,1013],{"id":1012},"what-i-actually-deliver","What I actually deliver",[28,1015,1016],{},"My Nuxt engagements cover the entire application lifecycle:",[59,1018,1019,1025,1031,1037,1046],{},[62,1020,1021,1024],{},[32,1022,1023],{},"Greenfield"," builds from A to Z (showcase sites, SaaS, marketplaces, internal tools)",[62,1026,1027,1030],{},[32,1028,1029],{},"Migration"," of ageing apps (Vue 2, Nuxt 2) to Nuxt 4",[62,1032,1033,1036],{},[32,1034,1035],{},"Audits"," of code, performance and SEO on existing projects",[62,1038,1039,1042,1043],{},[32,1040,1041],{},"Reinforcement"," of existing teams and developer ",[32,1044,1045],{},"upskilling",[62,1047,1048,1049,1052],{},"Long-term ",[32,1050,1051],{},"maintenance"," after delivery",[51,1054,1056],{"id":1055},"building-nuxt-applications-from-scratch","Building Nuxt applications from scratch",[28,1058,1059,1060,1063],{},"From architecture design to deployment, I build Nuxt applications focused on ",[32,1061,1062],{},"performance and SEO",":",[59,1065,1066,1069,1072,1075],{},[62,1067,1068],{},"server-side rendering (SSR) or static generation depending on your needs;",[62,1070,1071],{},"Core Web Vitals optimisation (LCP, INP, CLS) from day one;",[62,1073,1074],{},"structured data, sitemap and dynamic meta tags for search visibility;",[62,1076,1077],{},"accessibility and responsive design included by default.",[28,1079,1080],{},"A typical engagement starts with a scoping workshop (needs, scope, constraints), continues with an architecture proposal broken into milestones, then iterative deliveries — usually weekly — to your Git repository. You see the product move from week one and can test at every iteration.",[51,1082,1084],{"id":1083},"migration-from-vuejs-2-nuxt-2-to-nuxt-4","Migration from Vue.js 2 \u002F Nuxt 2 to Nuxt 4",[28,1086,1087,1088,1091],{},"Got an ageing Vue 2 or Nuxt 2 application? Now is the right time to act: ",[32,1089,1090],{},"Vue 2 has been end-of-life since late 2024"," — no security fixes, no evolution, and an ecosystem moving away.",[28,1093,1094,1095,1098],{},"I handle the ",[32,1096,1097],{},"migration to Nuxt 4"," progressively:",[287,1100,1101,1107,1113,1119],{},[62,1102,1103,1106],{},[32,1104,1105],{},"Audit of the existing code",": inventory of components, dependencies and friction points.",[62,1108,1109,1112],{},[32,1110,1111],{},"Migration plan",": steps deliverable without service interruption.",[62,1114,1115,1118],{},[32,1116,1117],{},"Refactoring",": Composition API, strict TypeScript, Nuxt 4 structure.",[62,1120,1121,1124],{},[32,1122,1123],{},"Modernisation along the way",": performance, SEO, tests.",[28,1126,1127],{},"The result: a healthy foundation for the next 5 years, without starting over.",[51,1129,1131],{"id":1130},"audit-and-optimisation","Audit and optimisation",[28,1133,1134],{},"Your Nuxt application exists but lags, or doesn't rank? I step in with audits:",[59,1136,1137,1143,1149],{},[62,1138,1139,1142],{},[32,1140,1141],{},"Performance",": Core Web Vitals analysis, bundle, images, fonts, caching.",[62,1144,1145,1148],{},[32,1146,1147],{},"Technical SEO",": rendering, meta tags, structured data, sitemap, canonicals.",[62,1150,1151,1154],{},[32,1152,1153],{},"Code quality",": technical debt, tests, typing, structure.",[28,1156,1157],{},"You leave with a prioritised report (impact × effort) and an action plan — which I can of course execute.",[51,1159,1161],{"id":1160},"why-a-freelancer-rather-than-a-nuxt-agency","Why a freelancer rather than a Nuxt agency?",[28,1163,1164],{},"You searched for a \"Nuxt agency\"? Good news: that's exactly what I do, differently.",[1166,1167,1168,1184],"table",{},[1169,1170,1171],"thead",{},[1172,1173,1174,1178,1181],"tr",{},[1175,1176,1177],"th",{},"Criterion",[1175,1179,1180],{},"Senior freelancer (me)",[1175,1182,1183],{},"Agency",[1185,1186,1187,1201,1214,1227,1240],"tbody",{},[1172,1188,1189,1195,1198],{},[1190,1191,1192],"td",{},[32,1193,1194],{},"Cost",[1190,1196,1197],{},"€450-550\u002Fday",[1190,1199,1200],{},"€700-900\u002Fday (often with junior profiles on the project)",[1172,1202,1203,1208,1211],{},[1190,1204,1205],{},[32,1206,1207],{},"Contact",[1190,1209,1210],{},"The developer who writes your code",[1190,1212,1213],{},"Project manager → lead → developer",[1172,1215,1216,1221,1224],{},[1190,1217,1218],{},[32,1219,1220],{},"Reactivity",[1190,1222,1223],{},"Same-day answer, immediate decisions",[1190,1225,1226],{},"Internal coordination delays",[1172,1228,1229,1234,1237],{},[1190,1230,1231],{},[32,1232,1233],{},"Flexibility",[1190,1235,1236],{},"Scope adjusted as we go",[1190,1238,1239],{},"Billed change requests",[1172,1241,1242,1247,1250],{},[1190,1243,1244],{},[32,1245,1246],{},"Ideal project size",[1190,1248,1249],{},"€5,000 to €50,000",[1190,1251,1252],{},"€50,000+ or multi-craft",[28,1254,1255,1256,1259],{},"For a reasonably sized project, the ",[32,1257,1258],{},"value of a senior freelancer"," boils down to three points: you talk to the person who writes the code, you pay for actual work (not the structure), and responsibility is single. Agencies remain relevant if you simultaneously need design, marketing and several teams — for pure development, it's rarely necessary.",[28,1261,1262,1263,49],{},"Curious about your project budget? ",[37,1264,1265],{"href":953},"Estimate it in 2 minutes",[51,1267,1269],{"id":1268},"how-much-does-nuxt-development-cost","How much does Nuxt development cost?",[28,1271,1272],{},"Real ranges (development only, maintenance excluded):",[1166,1274,1275,1288],{},[1169,1276,1277],{},[1172,1278,1279,1282,1285],{},[1175,1280,1281],{},"Project type",[1175,1283,1284],{},"Indicative budget",[1175,1286,1287],{},"Typical timeline",[1185,1289,1290,1301,1312,1323],{},[1172,1291,1292,1295,1298],{},[1190,1293,1294],{},"Nuxt showcase site \u002F portfolio",[1190,1296,1297],{},"€1,500 – 3,500",[1190,1299,1300],{},"1-2 weeks",[1172,1302,1303,1306,1309],{},[1190,1304,1305],{},"Application with CMS \u002F blog",[1190,1307,1308],{},"€3,500 – 6,000",[1190,1310,1311],{},"2-4 weeks",[1172,1313,1314,1317,1320],{},[1190,1315,1316],{},"SaaS MVP (auth, dashboard, API)",[1190,1318,1319],{},"€8,000 – 15,000",[1190,1321,1322],{},"4-10 weeks",[1172,1324,1325,1328,1331],{},[1190,1326,1327],{},"Vue 2 → Nuxt 4 migration",[1190,1329,1330],{},"€3,000 – 8,000",[1190,1332,1333],{},"2-6 weeks",[28,1335,1336,1337,1340],{},"Every project is unique — the simplest path is ",[37,1338,1339],{"href":953},"the quote simulator"," (free, instant, no commitment) or an initial call to scope precisely.",[51,1342,1344],{"id":1343},"my-complementary-stack","My complementary stack",[28,1346,1347],{},"I pair Nuxt with proven technologies to cover the whole chain:",[59,1349,1350,1356,1362,1368,1374],{},[62,1351,1352,1355],{},[32,1353,1354],{},"Backend",": NestJS (robust, typed APIs) or Nitro (endpoints built into Nuxt)",[62,1357,1358,1361],{},[32,1359,1360],{},"Database",": PostgreSQL (with Prisma or TypeORM)",[62,1363,1364,1367],{},[32,1365,1366],{},"Design",": Tailwind CSS, custom design systems",[62,1369,1370,1373],{},[32,1371,1372],{},"Deployment",": Docker, GitHub Actions CI\u002FCD, Caddy reverse proxy",[62,1375,1376,1379],{},[32,1377,1378],{},"Quality",": strict TypeScript, unit and integration tests",[28,1381,1382],{},"All delivered documented, on your repository, with reproducible infrastructure.",[51,1384,1386],{"id":1385},"a-reliable-technical-partner","A reliable technical partner",[28,1388,1389,1390,1394,1395,49],{},"I work in short iterations with a weekly check-in, deliver clean tested code, and stay available after production launch. Rather looking for a ",[37,1391,1393],{"href":1392},"\u002Fen\u002Fservices\u002Fdeveloppeur-freelance-lyon","freelance developer in Lyon"," for a long-term collaboration? ",[37,1396,949],{"href":948},{"title":96,"searchDepth":128,"depth":128,"links":1398},[1399,1400,1401,1402,1403,1404,1405,1406],{"id":1012,"depth":111,"text":1013},{"id":1055,"depth":111,"text":1056},{"id":1083,"depth":111,"text":1084},{"id":1130,"depth":111,"text":1131},{"id":1160,"depth":111,"text":1161},{"id":1268,"depth":111,"text":1269},{"id":1343,"depth":111,"text":1344},{"id":1385,"depth":111,"text":1386},"Trust your Nuxt project to an expert freelance developer: greenfield builds, Vue 2 to Nuxt 4 migration, SEO and performance audits. Transparent pricing, single point of contact, no agency overhead.",[1409,1412,1415,1418,1421,1424,1427,1430],{"question":1410,"answer":1411},"Why choose Nuxt over another framework?","Nuxt combines the best of Vue.js with proven conventions: server-side rendering (SSR) or static generation for top-tier SEO, automatic code-splitting for performance, and a mature module ecosystem (authentication, i18n, content). It's a long-term, fast and well-ranked choice.",{"question":1413,"answer":1414},"How much does it cost to build a Nuxt application?","A Nuxt application typically starts around €8,000 for a full MVP with a backend; a showcase site or simple app ranges from €1,500 to €3,500; a Vue 2 \u002F Nuxt 2 to Nuxt 4 migration from €3,000 to €8,000 depending on codebase size. My quote simulator gives you a tailored estimate in 2 minutes.",{"question":1416,"answer":1417},"Can you take over or migrate an existing Nuxt project?","Yes. I regularly take over existing projects: code audit, migration from Nuxt 2 or Vue 2 to Nuxt 4, refactoring to the Composition API and TypeScript — without breaking what already works or interrupting your business.",{"question":1419,"answer":1420},"How long does it take to build a Nuxt application?","Expect 2 to 4 weeks for a showcase site or small application, 4 to 10 weeks for a full MVP with backend, and more for a complex SaaS product. I deliver in short iterations so you see progress from the first weeks.",{"question":1422,"answer":1423},"Freelancer or Nuxt agency: which should I choose?","For a €5,000-50,000 project, a senior freelancer is almost always better value: a day rate 30-50% below agency rates, a single point of contact who actually writes the code, and direct communication with no commercial layers. Agencies become relevant for very large multi-team or multi-craft projects.",{"question":1425,"answer":1426},"Do you work with existing teams?","Yes. I integrate into existing teams (agile methods, Git, code review) or work fully autonomously. I can also help your developers level up on Nuxt, Vue 3 or DevOps practices.",{"question":1428,"answer":1429},"Vue 2 is end-of-life: what should I do with my app?","Vue 2 has been unmaintained since late 2024: no security fixes, no evolution. Migrating to Nuxt 4 secures your investment, improves performance and opens the door to the modern ecosystem. I run these migrations progressively, with no service interruption.",{"question":1431,"answer":1432},"Do you offer maintenance after delivery?","Yes. I offer maintenance packages (Nuxt and dependency updates, security, performance monitoring, small evolutions) to keep your application healthy over time — the best way to protect your initial investment.","Nuxt application development","lucide:code-xml",[1436,1437,34,969,1141,1438,993,994,1183,1439],"Nuxt","Vue.js","SEO","SSR",{},"\u002Fen\u002Fservices\u002Fdeveloppement-nuxt",{"title":1000,"description":1407},"en\u002Fservices\u002Fdeveloppement-nuxt","5JtIkyTdr8Qm4c1A4W1y8v5azX67HPTmN2DjaxsVnBU",{"id":1446,"title":1447,"body":1448,"description":1541,"extension":972,"faq":1542,"h1":1555,"icon":1556,"keywords":1557,"meta":1567,"navigation":18,"path":1392,"seo":1568,"stem":1569,"__hash__":1570},"services_en\u002Fen\u002Fservices\u002Fdeveloppeur-freelance-lyon.md","Freelance Developer in Lyon — Full Stack Vue.js, Nuxt & NestJS",{"type":25,"value":1449,"toc":1534},[1450,1456,1460,1463,1467,1492,1498,1502,1509,1513,1520,1524],[28,1451,1452,1453,1455],{},"Looking for a ",[32,1454,1393],{}," to bring your web product to life? I'm Louis Jourand, a Full Stack developer based in Lyon. I support startups and companies across the Lyon area and all of France on their most demanding projects.",[51,1457,1459],{"id":1458},"a-freelance-developer-in-lyon-available-on-site-or-remotely","A freelance developer in Lyon, available on-site or remotely",[28,1461,1462],{},"Based in Lyon, I can travel for kick-off workshops, demos or team syncs across the metropolitan area (Lyon 1st to 9th, Villeurbanne, Ecully, etc.). I also work fully remotely with clients anywhere in France.",[51,1464,1466],{"id":1465},"my-areas-of-expertise","My areas of expertise",[59,1468,1469,1475,1481,1486],{},[62,1470,1471,1474],{},[32,1472,1473],{},"Web applications"," in Vue.js, Nuxt, Angular or React",[62,1476,1477,1480],{},[32,1478,1479],{},"Robust APIs and backends"," in NestJS, Node.js, Python (Django\u002FFlask)",[62,1482,1483,1485],{},[32,1484,969],{},": Docker, CI\u002FCD, deployment industrialisation",[62,1487,1488,1491],{},[32,1489,1490],{},"AI integration"," and data processing",[28,1493,1494,1495,1497],{},"Want to go further with the Vue.js framework? Check out my ",[37,1496,1433],{"href":1441}," offer.",[51,1499,1501],{"id":1500},"why-work-with-a-freelancer","Why work with a freelancer?",[28,1503,1504,1505,1508],{},"Working with a freelance developer gives you ",[32,1506,1507],{},"responsiveness, flexibility and a single point of contact",". You get an expert who is personally invested in your project, without the overhead of an agency.",[51,1510,1512],{"id":1511},"sectors-and-project-types","Sectors and project types",[28,1514,1515,1516,1519],{},"I work on ",[32,1517,1518],{},"SaaS, ERPs, marketplaces, internal tools and AI-powered products",". My recent clients come from varied backgrounds: fintech, HR, scientific industry, logistics, video games.",[51,1521,1523],{"id":1522},"lets-discuss-your-project","Let's discuss your project",[28,1525,1526,1527,1530,1531,49],{},"Every engagement starts with a call to understand your needs and constraints. I then provide a clear estimate and a tailored breakdown. ",[37,1528,1529],{"href":948},"Contact me"," to get started, or ",[37,1532,1533],{"href":953},"estimate your project budget in 2 minutes",{"title":96,"searchDepth":128,"depth":128,"links":1535},[1536,1537,1538,1539,1540],{"id":1458,"depth":111,"text":1459},{"id":1465,"depth":111,"text":1466},{"id":1500,"depth":111,"text":1501},{"id":1511,"depth":111,"text":1512},{"id":1522,"depth":111,"text":1523},"Freelance developer based in Lyon, I help startups and companies design modern, high-performance web applications. Available on-site or remotely.",[1543,1546,1549,1552],{"question":1544,"answer":1545},"How much does a freelance developer in Lyon cost?","My rates depend on the duration, complexity and context of the engagement. I work on a daily rate (TJM) or a fixed price, whichever suits you best. Every quote is detailed with no surprises: you can get a first estimate in 2 minutes with my quote simulator.",{"question":1547,"answer":1548},"What are the benefits of working with a freelancer in Lyon?","You get a single point of contact personally invested in your project: responsiveness, flexibility and expertise, without agency overhead. And if you're in the Lyon area, we can meet in person for kick-off workshops, demos and team syncs.",{"question":1550,"answer":1551},"Do you only work in Lyon?","No. I'm based in Lyon and happy to travel across the region for key project moments, but I also work fully remotely with clients anywhere in France and internationally.",{"question":1553,"answer":1554},"How does a freelance collaboration start?","With a free first call to understand your needs, constraints and goals. I then provide a clear estimate and a breakdown into milestones. I work in short iterations with regular check-ins, and deliver tested, documented code to your repository.","Freelance developer in Lyon","lucide:map-pin",[1558,1559,1560,1561,1562,1563,1564,1565,1566],"AI","Automation","Data","Productivity","SME","Chatbot","Workflow","Dashboard","Freelance",{},{"title":1447,"description":1541},"en\u002Fservices\u002Fdeveloppeur-freelance-lyon","KkXHAuxDJpwM1P3huIl8g66TseDjeqMMwmpx36KAyQ0",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1572},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m12 19l-7-7l7-7m7 7H5\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":1574},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m18 16l4-4l-4-4M6 8l-4 4l4 4m8.5-12l-5 16\"\u002F>",1789936609941]