<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: lvn1</title>
    <description>The latest articles on DEV Community by lvn1 (@lvn1).</description>
    <link>https://dev.to/lvn1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F417968%2Ff4abe59c-01f4-446b-b7e5-b08daf844e1a.jpeg</url>
      <title>DEV Community: lvn1</title>
      <link>https://dev.to/lvn1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lvn1"/>
    <language>en</language>
    <item>
      <title>Monitor websites changes with Firecrawl Observer and Docker</title>
      <dc:creator>lvn1</dc:creator>
      <pubDate>Mon, 22 Sep 2025 02:00:59 +0000</pubDate>
      <link>https://dev.to/lvn1/monitor-websites-changes-with-firecrawl-observer-and-docker-4iji</link>
      <guid>https://dev.to/lvn1/monitor-websites-changes-with-firecrawl-observer-and-docker-4iji</guid>
      <description>&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Linux/Unix.&lt;/li&gt;
&lt;li&gt;Docker &amp;amp; Docker compose&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/firecrawl/firecrawl-observer" rel="noopener noreferrer"&gt;Firecrawl Observer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://convex.dev" rel="noopener noreferrer"&gt;Convex&lt;/a&gt; account with production deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo63lvqh5tqdrn3zl6u7z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo63lvqh5tqdrn3zl6u7z.png" alt=" " width="800" height="773"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Clone, initialse and prepare the environment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mendableai/firecrawl-observer.git
&lt;span class="nb"&gt;cd &lt;/span&gt;firecrawl-observer
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npx convex dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Open a new terminal&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Here we set up authentication for production&lt;/span&gt;
npx @convex-dev/auth &lt;span class="nt"&gt;--prod&lt;/span&gt;

&lt;span class="c"&gt;# Set encryption key (REQUIRED - only run if not already set)&lt;/span&gt;
npx convex &lt;span class="nb"&gt;env set &lt;/span&gt;ENCRYPTION_KEY &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--prod&lt;/span&gt;

&lt;span class="c"&gt;# Verify all required variables are set&lt;/span&gt;
npx convex &lt;span class="nb"&gt;env &lt;/span&gt;list &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Required variables should include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SITE_URL&lt;/code&gt; ✓ (already set)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JWT_PRIVATE_KEY&lt;/code&gt; ✓ (already set) &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JWKS&lt;/code&gt; ✓ (already set)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ENCRYPTION_KEY&lt;/code&gt; (set this now)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploy Convex functions to production&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex deploy &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Update Environment File. Replace YOUR_PUBLIC_URL with the domain provided by convex.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create/update .env file&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
NEXT_PUBLIC_CONVEX_URL=YOUR_PUBLIC_URL
NODE_ENV=production
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Update Dockerfile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build the application&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Declare build-time arguments&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_CONVEX_URL&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;

&lt;span class="c"&gt;# Set environment variables for build&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_CONVEX_URL=$NEXT_PUBLIC_CONVEX_URL&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=$NODE_ENV&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; yarn.lock &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;yarn &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; package-lock.json &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;npm ci &lt;span class="nt"&gt;--only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pnpm-lock.yaml &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;yarn global add pnpm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm i &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt; &lt;span class="nt"&gt;--prod&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;else &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Lockfile not found."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Copy source and build&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Stage 2: Production runtime&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Install curl for health checks&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Create non-root user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; 1001 nodejs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 1001 nextjs

&lt;span class="c"&gt;# Copy built application with proper ownership&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/public ./public&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next ./.next&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/package.json ./package.json&lt;/span&gt;

&lt;span class="c"&gt;# Switch to non-root user&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nextjs&lt;/span&gt;

&lt;span class="c"&gt;# Expose port and set environment&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=3000&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;

&lt;span class="c"&gt;# Health check&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=10s --start-period=40s --retries=3 \&lt;/span&gt;
    CMD curl -f http://localhost:3000/api/health || exit 1

&lt;span class="c"&gt;# Start application&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["npm", "start"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Update docker-compose.yml (Production Ready)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;firecrawl-observer-app&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=production&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-network&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/api/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;40s&lt;/span&gt;
    &lt;span class="c1"&gt;# Resource limits (adjust based on your server)&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1G&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;
        &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.25'&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5001:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/nginx.htpasswd:/etc/nginx/conf.d/nginx.htpasswd:ro&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-network&lt;/span&gt;
    &lt;span class="c1"&gt;# Security and performance&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128M&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.1'&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="c1"&gt;# Optional: Volumes for persistence&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nginx_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Optimize Nginx Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update nginx.conf with production optimizations&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nginx/nginx.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
# Upstream definition
upstream app_backend {
    server app:3000;
    keepalive 32;
}

server {
    listen 80;
    server_name _;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Basic auth for all routes
    auth_basic "Restricted Content";
    auth_basic_user_file /etc/nginx/conf.d/nginx.htpasswd;

    # Health check endpoint (bypass auth)
    location /api/health {
        access_log off;
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }

    # Main application
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade &lt;/span&gt;&lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="sh"&gt;;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="sh"&gt;;
        proxy_set_header X-Real-IP &lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="sh"&gt;;
        proxy_set_header X-Forwarded-For &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="sh"&gt;;
        proxy_set_header X-Forwarded-Proto &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="sh"&gt;;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffer settings
        proxy_buffering on;
        proxy_buffer_size 8k;
        proxy_buffers 8 8k;

        # Cache static assets
        location ~* &lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sh"&gt;(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="sh"&gt;{
            expires 1y;
            add_header Cache-Control "public, immutable";
            proxy_pass http://app_backend;
        }
    }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Add Health Check Endpoint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create health check API route&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src/app/api/health
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; src/app/api/health/route.ts &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
import { NextResponse } from 'next/server';

export async function GET() {
  try {
    // Basic health check - can be expanded to check dependencies
    return NextResponse.json({ 
      status: 'healthy',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
      environment: process.env.NODE_ENV || 'development'
    }, { status: 200 });
  } catch (error) {
    return NextResponse.json({ 
      status: 'unhealthy',
      error: 'Health check failed',
      timestamp: new Date().toISOString()
    }, { status: 503 });
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: Deploy with Zero-Downtime Strategy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop current containers gracefully&lt;/span&gt;
docker-compose down

&lt;span class="c"&gt;# Clean up old images (optional, saves disk space)&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Build and deploy with improved logging&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Monitor startup&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for services to start..."&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;10

&lt;span class="c"&gt;# Check service health&lt;/span&gt;
docker-compose ps
docker-compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8: Verification and Monitoring. Replace SERVER_IP &amp;amp; PORT with yours
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test health endpoint&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://SERVER_IP:PORT/api/health

&lt;span class="c"&gt;# Test full application (will prompt for basic auth)&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://SERVER_IP:PORT/

&lt;span class="c"&gt;# Monitor logs in real-time&lt;/span&gt;
docker-compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20

&lt;span class="c"&gt;# Check resource usage&lt;/span&gt;
docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production Monitoring Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create monitoring script&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; monitor.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
echo "=== Container Status ==="
docker-compose ps

echo -e "&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;=== Health Check ==="
curl -s http://localhost:3000/api/health | jq '.' 2&amp;gt;/dev/null || echo "Health check failed"

echo -e "&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;=== Resource Usage ==="
docker stats --no-stream --format "table {{.Container}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;{{.CPUPerc}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;{{.MemUsage}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;{{.MemPerc}}"

echo -e "&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;=== Recent Logs ==="
docker-compose logs --tail=5 app
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x monitor.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Automated Deployment Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create deployment script for future updates&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; deploy.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
set -e

echo "Starting deployment..."

# Pull latest changes (if using git)
# git pull origin main

# Deploy Convex functions
echo "Deploying Convex functions..."
npx convex deploy --prod

# Build and deploy containers
echo "Building and deploying containers..."
docker-compose down
docker-compose up --build -d

# Wait for health check
echo "Waiting for application to be healthy..."
for i in {1..30}; do
    if curl -f http://localhost:3000/api/health &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
        echo "Application is healthy!"
        break
    fi
    echo "Waiting... (&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="sh"&gt;/30)"
    sleep 10
done

echo "Deployment complete!"
docker-compose ps
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Non-root containers&lt;/strong&gt;: App runs as user &lt;code&gt;nextjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Basic authentication&lt;/strong&gt;: Password protection via nginx&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Security headers&lt;/strong&gt;: XSS, CSRF, and frame protection&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Resource limits&lt;/strong&gt;: Memory and CPU constraints&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Health checks&lt;/strong&gt;: Automated container health monitoring&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Encrypted secrets&lt;/strong&gt;: API keys stored in Convex cloud&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;HTTPS&lt;/strong&gt;: Consider adding SSL/TLS for production&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Firewall&lt;/strong&gt;: Ensure only necessary ports are open&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Quick Diagnostics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full system check&lt;/span&gt;
./monitor.sh

&lt;span class="c"&gt;# View all logs&lt;/span&gt;
docker-compose logs

&lt;span class="c"&gt;# Restart specific service&lt;/span&gt;
docker-compose restart app

&lt;span class="c"&gt;# Force rebuild&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common Issues
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Auth errors&lt;/strong&gt;: Check Convex environment variables with &lt;code&gt;npx convex env list --prod&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build failures&lt;/strong&gt;: Verify &lt;code&gt;.env&lt;/code&gt; file and &lt;code&gt;NEXT_PUBLIC_CONVEX_URL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health check failures&lt;/strong&gt;: Check if port 3000 is accessible inside container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx issues&lt;/strong&gt;: Verify &lt;code&gt;nginx.htpasswd&lt;/code&gt; file permissions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you need a cloud server: &lt;a href="https://m.do.co/c/2e46c4b63f1d" rel="noopener noreferrer"&gt;$200 Digital Ocean Credit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms0f2n5tjlgkrqt6vwku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms0f2n5tjlgkrqt6vwku.png" alt=" " width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpj44ka4axyht9renz4b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpj44ka4axyht9renz4b.png" alt=" " width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8zu53uczwpb3htmfeb3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8zu53uczwpb3htmfeb3.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a surveillance system with an old Raspberry Pi</title>
      <dc:creator>lvn1</dc:creator>
      <pubDate>Wed, 17 Sep 2025 03:30:57 +0000</pubDate>
      <link>https://dev.to/lvn1/how-to-set-up-a-raspberry-pi-camera-with-shinobi-for-reliable-247-cctv-monitoring-243l</link>
      <guid>https://dev.to/lvn1/how-to-set-up-a-raspberry-pi-camera-with-shinobi-for-reliable-247-cctv-monitoring-243l</guid>
      <description>&lt;p&gt;This guide walks you through setting up a Raspberry Pi camera with &lt;a href="https://github.com/shinobicctv" rel="noopener noreferrer"&gt;Shinobi Open Source CCTV software&lt;/a&gt;. We'll address common hardware, networking, and performance issues to create a stable monitoring solution that actually works on resource constrained devices like the Raspberry Pi 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardware:&lt;/strong&gt; Raspberry Pi (tested on Pi 2), compatible CSI camera module (OV5647), reliable power supply, 32GB+ SD card&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software:&lt;/strong&gt; Fresh Raspberry Pi OS installation, SSH client, network access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network:&lt;/strong&gt; Local network access and basic router configuration knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfbjanl48tlf5b8pdiz3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfbjanl48tlf5b8pdiz3.jpeg" alt="My Setup" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Initial Raspberry Pi Setup
&lt;/h2&gt;

&lt;p&gt;Start with a proper foundation to avoid headaches later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Configuration
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flash Raspberry Pi OS:&lt;/strong&gt; Use the official Raspberry Pi Imager for a clean Raspberry Pi OS Lite installation (headless) or with Desktop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable SSH:&lt;/strong&gt; During imaging, enable SSH in the advanced options, or enable it post-boot with &lt;code&gt;sudo raspi-config&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;First Boot:&lt;/strong&gt; Connect via SSH and update everything:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Camera Hardware Verification
&lt;/h3&gt;

&lt;p&gt;Before installing anything, verify your camera actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;libcamera-hello &lt;span class="nt"&gt;-t&lt;/span&gt; 2000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a 2-second preview (on connected display) or the command should complete without "camera not found" errors. If this fails, check your ribbon cable connection - everything else depends on this working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Installing Shinobi
&lt;/h2&gt;

&lt;p&gt;The official installer handles most of the heavy lifting, but you need to know the specific steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run the Official Installer
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Switch to root and run installer:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;su
   sh &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://cdn.shinobi.video/installers/shinobi-install.sh&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Select "Ubuntu Touchless"&lt;/strong&gt; when prompted - this works best for Raspberry Pi installations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Handle IPv6 prompt:&lt;/strong&gt; If asked about disabling IPv6, choose "Yes" to avoid connectivity issues during installation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Critical Network Configuration Fix
&lt;/h3&gt;

&lt;p&gt;Here's the part most guides miss: Shinobi will only bind to IPv6 by default, making it inaccessible from other devices on your network.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Edit the configuration file:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;nano /home/Shinobi/conf.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the IP parameter&lt;/strong&gt; (not "host") at the beginning of the JSON:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.20.15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;192.168.20.15&lt;/code&gt; with your actual Pi's IP address.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Restart Shinobi:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;pm2 restart camera
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify it's working:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;netstat &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; :8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both &lt;code&gt;tcp&lt;/code&gt; and &lt;code&gt;tcp6&lt;/code&gt; entries, not just &lt;code&gt;tcp6&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial Shinobi Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access the superuser panel:&lt;/strong&gt; Open &lt;code&gt;http://YOUR_PI_IP:8080/super&lt;/code&gt; in your browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Default credentials:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Username: &lt;code&gt;admin@shinobi.video&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Password: &lt;code&gt;admin&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create your admin account&lt;/strong&gt; through the superuser panel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log into main interface:&lt;/strong&gt; Access &lt;code&gt;http://YOUR_PI_IP:8080&lt;/code&gt; (without &lt;code&gt;/super&lt;/code&gt;) using your new credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Change superuser credentials immediately&lt;/strong&gt; in the Preferences tab for security.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Camera Streaming Pipeline
&lt;/h2&gt;

&lt;p&gt;Create a reliable video stream that Shinobi can actually connect to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Required Packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-bad netcat-openbsd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the Streaming Script
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the script file:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   nano /home/first/streamscript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add this pipeline&lt;/strong&gt; (optimized for reliability over quality):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

   &lt;span class="c"&gt;# Reliable MJPEG stream using software encoding&lt;/span&gt;
   &lt;span class="c"&gt;# Hardware encoding isn't available on older Pi models&lt;/span&gt;

   &lt;span class="nv"&gt;BOUNDARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--boundary"&lt;/span&gt;

   &lt;span class="nv"&gt;PIPELINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gst-launch-1.0 -q libcamerasrc ! video/x-raw,width=320,height=240,framerate=10/1 ! jpegenc ! multipartmux boundary=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOUNDARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ! filesink location=/dev/stdout"&lt;/span&gt;

   &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
     &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"HTTP/1.0 200 OK&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;Content-Type: multipart/x-mixed-replace; boundary=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOUNDARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIPELINE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt; | nc &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8090
   &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make it executable:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /home/first/streamscript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Background Service Setup
&lt;/h2&gt;

&lt;p&gt;Set up automatic startup and crash recovery using systemd user services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the Service
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create service directory:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/systemd/user/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create service file:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   nano ~/.config/systemd/user/shinobi-stream.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add service configuration:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;   &lt;span class="nn"&gt;[Unit]&lt;/span&gt;
   &lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Shinobi Camera Streamer&lt;/span&gt;
   &lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;graphical-session.target&lt;/span&gt;
   &lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;graphical-session.target&lt;/span&gt;

   &lt;span class="nn"&gt;[Service]&lt;/span&gt;
   &lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/first/streamscript&lt;/span&gt;
   &lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
   &lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;

   &lt;span class="nn"&gt;[Install]&lt;/span&gt;
   &lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable and Start
&lt;/h3&gt;

&lt;p&gt;Run these commands &lt;strong&gt;without sudo&lt;/strong&gt; (important for user services):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;shinobi-stream.service
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start shinobi-stream.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable Auto-Start on Boot
&lt;/h3&gt;

&lt;p&gt;This crucial step makes the service start even when you're not logged in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reboot your Pi to test everything starts correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Configure Shinobi Monitor
&lt;/h2&gt;

&lt;p&gt;Connect Shinobi to your camera stream.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add new monitor:&lt;/strong&gt; Click the &lt;code&gt;+&lt;/code&gt; icon in Shinobi dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Connection settings:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input Type:&lt;/strong&gt; &lt;code&gt;MJPEG&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full URL Path:&lt;/strong&gt; &lt;code&gt;http://127.0.0.1:8090&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stream settings:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frame Rate:&lt;/strong&gt; &lt;code&gt;10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Width:&lt;/strong&gt; &lt;code&gt;320&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Height:&lt;/strong&gt; &lt;code&gt;240&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Save and test:&lt;/strong&gt; You should see live video immediately.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 6: Performance Tuning
&lt;/h2&gt;

&lt;p&gt;Resource-constrained hardware requires careful balance between quality and performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the Limitations
&lt;/h3&gt;

&lt;p&gt;Older Raspberry Pi models lack hardware video encoders that GStreamer can access. Everything runs on CPU, so optimization is critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tuning Parameters
&lt;/h3&gt;

&lt;p&gt;Adjust the &lt;code&gt;PIPELINE&lt;/code&gt; variable in &lt;code&gt;/home/first/streamscript&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For better performance (lower CPU usage):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PIPELINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gst-launch-1.0 -q libcamerasrc ! video/x-raw,width=160,height=120,framerate=5/1 ! jpegenc quality=50 ! multipartmux boundary=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOUNDARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ! filesink location=/dev/stdout"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For better quality (higher CPU usage):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PIPELINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gst-launch-1.0 -q libcamerasrc ! video/x-raw,width=640,height=480,framerate=15/1 ! jpegenc quality=90 ! multipartmux boundary=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOUNDARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ! filesink location=/dev/stdout"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor CPU usage with &lt;code&gt;htop&lt;/code&gt; and adjust accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Can't Access from Other Devices"
&lt;/h3&gt;

&lt;p&gt;This is usually the IPv4/IPv6 binding issue:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check what Shinobi is listening on:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;netstat &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; :8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If you only see &lt;code&gt;tcp6&lt;/code&gt;, check your config:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-A3&lt;/span&gt; &lt;span class="nt"&gt;-B3&lt;/span&gt; &lt;span class="s1"&gt;'"ip"'&lt;/span&gt; /home/Shinobi/conf.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make sure you used &lt;code&gt;"ip"&lt;/code&gt; not &lt;code&gt;"host"&lt;/code&gt; parameter.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  "Stream Won't Start"
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check service status:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status shinobi-stream.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;View service logs:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; shinobi-stream.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test camera directly:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   libcamera-hello &lt;span class="nt"&gt;-t&lt;/span&gt; 2000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  "High CPU Usage"
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lower resolution and framerate&lt;/strong&gt; in your streamscript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce JPEG quality&lt;/strong&gt; by adding &lt;code&gt;quality=50&lt;/code&gt; to &lt;code&gt;jpegenc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for multiple streams&lt;/strong&gt; running accidentally.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Network Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change default Shinobi credentials&lt;/strong&gt; immediately after setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't expose port 8080&lt;/strong&gt; to the internet via router port forwarding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use strong passwords&lt;/strong&gt; for all accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider firewall rules&lt;/strong&gt; to limit access to specific IP ranges:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 192.168.0.0/16 to any port 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  System Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change default Pi password&lt;/strong&gt; if you haven't already&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep system updated&lt;/strong&gt; with regular &lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor access logs&lt;/strong&gt; in Shinobi's admin panel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;This setup prioritizes reliability over fancy features. You'll have a stable, 24/7 monitoring solution that actually works on older hardware. The key insights that make this work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use software encoding&lt;/strong&gt; - hardware encoders aren't reliably available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the IPv4 binding issue&lt;/strong&gt; - use &lt;code&gt;"ip"&lt;/code&gt; parameter, not &lt;code&gt;"host"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proper systemd service setup&lt;/strong&gt; - ensures automatic recovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conservative performance settings&lt;/strong&gt; - prevents crashes under load&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have this basic setup running reliably, you can experiment with higher resolutions, multiple cameras, or advanced Shinobi features.&lt;/p&gt;

</description>
      <category>security</category>
      <category>raspberrypi</category>
      <category>linux</category>
      <category>computervision</category>
    </item>
    <item>
      <title>A Chrome Extension That Redesigns Any Website in Seconds - WebImgs</title>
      <dc:creator>lvn1</dc:creator>
      <pubDate>Mon, 18 Aug 2025 01:37:40 +0000</pubDate>
      <link>https://dev.to/lvn1/a-chrome-extension-that-redesigns-any-website-in-seconds-webimgs-4f3l</link>
      <guid>https://dev.to/lvn1/a-chrome-extension-that-redesigns-any-website-in-seconds-webimgs-4f3l</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is my submission for &lt;a href="https://dev.to/deved/build-apps-with-google-ai-studio"&gt;DEV Education Track: Build Apps with Google AI Studio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Have you ever stumbled across a promising startup's website, only to find it filled with generic stock photos and placeholder logos? Or maybe you're a developer who's brilliant at backend code but struggles with visual design?&lt;/p&gt;

&lt;p&gt;What if there was a way to instantly transform any website's visuals with AI-generated images that actually &lt;em&gt;belong&lt;/em&gt; there?&lt;/p&gt;

&lt;p&gt;That's how &lt;strong&gt;WebImgs&lt;/strong&gt; was born – a Chrome extension that scans any website, understands its design DNA, and regenerates every visual asset to match perfectly. No more mismatched stock photos or bland placeholders. Just click, scan, and watch a website come to life.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic Behind the Curtain
&lt;/h2&gt;

&lt;p&gt;WebImgs isn't just another AI image generator – it's a design detective. Here's what makes it special:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;Universal Scanning&lt;/strong&gt; - Detects logos, hero images, banners, icons, and background elements on any website&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Style Aware Generation&lt;/strong&gt; - Analyzes the website's color scheme, typography, and design patterns&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Real Time Preview&lt;/strong&gt; - Toggle between original and AI generated images instantly
&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Selective Generation&lt;/strong&gt; - Choose exactly which assets to regenerate&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Batch Export&lt;/strong&gt; - Download all generated assets as a ZIP file&lt;/li&gt;
&lt;li&gt;⌨️ &lt;strong&gt;Keyboard Shortcuts&lt;/strong&gt; - Quick generation with Ctrl+Shift+G&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Google AI Studio
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0cpydewshm1iix2fuspb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0cpydewshm1iix2fuspb.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the core prompt that started it all:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You are building a Chrome extension that automatically detects and generates ALL visual assets for any website using AI image generation. The extension transforms text-only or placeholder-filled websites into fully designed, visually cohesive sites.&lt;/p&gt;

&lt;p&gt;Core functionality: Scan any webpage → Detect all image zones → Categorize assets needed → Generate consistent visuals → Replace/export images.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We'll build this step by step. After each major step, I'll verify completion before proceeding."&lt;/p&gt;

&lt;p&gt;Through countless iterations in AI Studio, this evolved into a sophisticated four step process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Website DNA Analysis&lt;/strong&gt; - Extract colors, typography, spacing patterns, and overall aesthetic mood&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Categorization&lt;/strong&gt; - Identify whether each image is a logo, hero banner, product shot, or decorative element&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual Prompt Generation&lt;/strong&gt; - Create highly specific prompts for &lt;a href="https://cloud.google.com/vertex-ai/generative-ai/docs/image/overview" rel="noopener noreferrer"&gt;Google's Imagen API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency Enforcement&lt;/strong&gt; - Ensure all generated assets feel like they belong to the same brand family&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The difference between "generate a logo" and "generate a minimalist tech startup logo with navy blue and white color scheme, clean sans-serif typography influence, and modern geometric styling" became the difference between random images and &lt;em&gt;perfect&lt;/em&gt; images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Here's how WebImgs works in practice:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Scan Any Website
&lt;/h3&gt;

&lt;p&gt;Click the extension icon and hit "Scan Page" - WebImgs instantly identifies every image zone on the page, from logos in the header to product shots in the footer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fda9ir4dqpfdllu8clidn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fda9ir4dqpfdllu8clidn.png" alt=" " width="800" height="1629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Visual Asset Detection
&lt;/h3&gt;

&lt;p&gt;The extension overlays colored zones showing exactly what it detected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟡 Logos and branding elements&lt;/li&gt;
&lt;li&gt;🔵 Hero images and banners
&lt;/li&gt;
&lt;li&gt;🟢 Product and gallery images&lt;/li&gt;
&lt;li&gt;🟣 Icons and decorative elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foyyuyvfmjzqcz6p7kr4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foyyuyvfmjzqcz6p7kr4d.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: AI Magic Happens
&lt;/h3&gt;

&lt;p&gt;Here's where Google AI Studio's prompt engineering shines. Instead of generic requests, WebImgs generates contextual prompts like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Professional fintech logo with navy blue gradient, modern sans-serif influence, clean geometric design, startup aesthetic"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Hero banner for financial dashboard app, contemporary UI design language, subtle shadows, 1200x400 format"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Instant Transformation
&lt;/h3&gt;

&lt;p&gt;The results? Images that look like they were designed by the original team. Toggle between versions to see the dramatic difference – same layout, but now it feels like a real brand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllrwc2rmcsiyy1avzjzh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllrwc2rmcsiyy1avzjzh.png" alt="Generated assets" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Ready for Production
&lt;/h3&gt;

&lt;p&gt;Export everything as an organized ZIP file. Each asset is properly named and sized, ready to drop into your codebase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpovksy9oux8ik22rk55r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpovksy9oux8ik22rk55r.png" alt="Before and after comparison" width="658" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If this gets enough interest, I will release the extension in the Chrome webstore and give out some free credits. I have already secured the domain, just haven't launched the extension yet&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself&lt;/strong&gt;: &lt;a href="https://webimgs.com" rel="noopener noreferrer"&gt;WebImgs Chrome Extension&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Core Technology:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript &amp;amp; Webpack&lt;/strong&gt; for the Chrome extension architecture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google AI Studio&lt;/strong&gt; for rapid prompt development and testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Vertex AI Imagen API&lt;/strong&gt; for high-quality image generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase backend&lt;/strong&gt; handling authentication and usage credits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Secret Sauce:&lt;/strong&gt; WebImgs doesn't just generate random images. It's essentially a design detective that analyzes websites like a human designer would by extracting color palettes, understanding typography choices, recognizing layout patterns, and even inferring business context. This analysis becomes the foundation for creating prompts that generate visually cohesive assets that actually belong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;p&gt;Three months ago, I was frustrated. Every side project I built looked like it was designed by a developer (because it was). Stock photos felt fake, placeholder logos screamed "temporary," and I couldn't afford a designer for every idea.&lt;/p&gt;

&lt;p&gt;That frustration led me down a rabbit hole that changed how I think about AI.&lt;/p&gt;

&lt;p&gt;Building WebImgs taught me that the real challenge isn't generating images – it's generating the &lt;em&gt;right&lt;/em&gt; images. Here's the tech stack that makes it work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself soon&lt;/strong&gt;: &lt;a href="https://webimgs.com" rel="noopener noreferrer"&gt;WebImgs Chrome Extension&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want to see more? Check out the &lt;a href="https://cloud.google.com/vertex-ai/generative-ai/pricing#imagen-models" rel="noopener noreferrer"&gt;Imagen API pricing&lt;/a&gt; to understand the cost structure behind AI-powered design.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>deved</category>
      <category>learngoogleaistudio</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>How to Update AWS Amplify After Renaming Your GitHub Repository</title>
      <dc:creator>lvn1</dc:creator>
      <pubDate>Thu, 16 Jan 2025 15:48:25 +0000</pubDate>
      <link>https://dev.to/lvn1/how-to-update-aws-amplify-after-renaming-your-github-repository-3pi9</link>
      <guid>https://dev.to/lvn1/how-to-update-aws-amplify-after-renaming-your-github-repository-3pi9</guid>
      <description>&lt;p&gt;Here's a helpful guide for developers who need to update their Amplify app after renaming a GitHub repository:&lt;/p&gt;

&lt;h1&gt;
  
  
  How to Update AWS Amplify After Renaming Your GitHub Repository
&lt;/h1&gt;

&lt;p&gt;When you rename a GitHub repository connected to AWS Amplify, your builds will fail because Amplify can't find the original repository. Here's a guide to fix this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Already have an app hosted in AWS Amplify&lt;/li&gt;
&lt;li&gt;GitHub account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Generate a GitHub Access Token
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Go to GitHub.com, sign in. Navigate to Settings → Developer settings → Personal access tokens → Tokens (classic)&lt;/li&gt;
&lt;li&gt;Click "Generate new token (classic)". Give it a descriptive name (e.g., "Amplify Repo Access")&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;repo&lt;/code&gt; scope. Click "Generate token" and copy it immediately (you won't see it again)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: Find Your Amplify App ID
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the AWS Amplify Console&lt;a href="https://us-east-1.console.aws.amazon.com/amplify/apps" rel="noopener noreferrer"&gt;Change it to your region prefix&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Select your app,  look for the app ID. It starts with 'd' followed by letters and numbers (e.g., &lt;code&gt;d2x3uf5yeo5smt&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Update Repository Connection
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open AWS CloudShell in your browser, click on the terminal on this page: &lt;a href="https://us-east1.console.aws.amazon.com/amplify/apps" rel="noopener noreferrer"&gt;Change it to your region prefix&lt;/a&gt; or your local terminal&lt;/li&gt;
&lt;li&gt;Run the following command, replacing the placeholders:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws amplify update-app \
  --app-id YOUR_APP_ID \
  --repository https://github.com/USERNAME/NEW-REPO-NAME \
  --access-token YOUR_GITHUB_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Verify the Connection
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Go back to the Amplify Console&lt;/li&gt;
&lt;li&gt;Check your app's build history&lt;/li&gt;
&lt;li&gt;Trigger a new build to verify the connection works&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Security Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Revoke the GitHub token after successful connection if you won't need it again&lt;/li&gt;
&lt;li&gt;Never commit or share your GitHub access token&lt;/li&gt;
&lt;li&gt;Use the minimum required scopes for the token&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;If you encounter issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify your app ID is correct&lt;/li&gt;
&lt;li&gt;Ensure the GitHub token has the correct permissions&lt;/li&gt;
&lt;li&gt;Check that the repository URL is exact (case-sensitive)&lt;/li&gt;
&lt;li&gt;Make sure you have admin access to both Amplify and the GitHub repository&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This process doesn't affect your local development environment&lt;/li&gt;
&lt;li&gt;Your build settings and environment variables remain unchanged&lt;/li&gt;
&lt;li&gt;The GitHub repository history is preserved&lt;/li&gt;
&lt;li&gt;If you have multiple branches, they will all be reconnected automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By following these steps, you can quickly update your Amplify app to point to your renamed GitHub repository without losing any configuration or deployment history.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Google Authentication in a Chrome Extension with Firebase</title>
      <dc:creator>lvn1</dc:creator>
      <pubDate>Thu, 05 Sep 2024 18:05:59 +0000</pubDate>
      <link>https://dev.to/lvn1/google-authentication-in-a-chrome-extension-with-firebase-2bmo</link>
      <guid>https://dev.to/lvn1/google-authentication-in-a-chrome-extension-with-firebase-2bmo</guid>
      <description>&lt;h4&gt;
  
  
  We're writing this guide because the official one by Google is missing a few important steps, linked below:
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Official Google guide:&lt;/strong&gt; &lt;a href="https://firebase.google.com/docs/auth/web/chrome-extension" rel="noopener noreferrer"&gt;Authenticate with Firebase in a Chrome extension&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our guide:&lt;/strong&gt; &lt;a href="https://dev.to/lvn1/google-authentication-in-a-chrome-extension-with-firebase-2bmo"&gt;Dev.to&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can clone the repo here or start from scratch by following the tutorial below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lvn1/chrome-extension-firebase-auth" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  If you find this guide or repo helpful, please star it:)
&lt;/h4&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;This will work on any operating system. For the purposes of this guide we'll be using Mac OS&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Chrome browser&lt;/li&gt;
&lt;li&gt;A Google account (Will be linked to Firebase and Chrome Web Store)&lt;/li&gt;
&lt;li&gt;A Chrome web store developer account ($5 one time fee)&lt;/li&gt;
&lt;li&gt;Node.js and npm installed (Latest or LTS version)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Project Structure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;a)&lt;/strong&gt; Create a new directory for your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir firebase-chrome-auth
cd firebase-chrome-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;b)&lt;/strong&gt; Create two subdirectories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir chrome-extension
mkdir firebase-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Set up the Firebase Project
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;a)&lt;/strong&gt; Go to the Firebase Console.&lt;br&gt;
&lt;strong&gt;b)&lt;/strong&gt; Click "Add project" and follow the steps to create a new project.&lt;br&gt;
&lt;strong&gt;c)&lt;/strong&gt; Once created, click on "Web" to add a web app to your project.&lt;br&gt;
&lt;strong&gt;d)&lt;/strong&gt; Register your app with a nickname (e.g., "Chrome Extension Auth"). Also select hosting, we'll add it later in command line.&lt;br&gt;
&lt;strong&gt;e)&lt;/strong&gt; Copy the Firebase configuration object. You'll need this later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const firebaseConfig = {
  apiKey: "example",
  authDomain: "example.firebaseapp.com",
  projectId: "example",
  storageBucket: "example",
  messagingSenderId: "example",
  appId: "example"
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;f)&lt;/strong&gt; Navigate to the firebase-project directory &lt;code&gt;cd firebase-project&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;g)&lt;/strong&gt; Initialize a new npm project &lt;code&gt;npm init -y&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;h)&lt;/strong&gt; Create a public dir &lt;code&gt;mkdir public&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;i)&lt;/strong&gt; Create an index.html file &lt;br&gt;
&lt;em&gt;firebase-project/public/index.html&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
 &amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;Firebase Auth for Chrome Extension&amp;lt;/title&amp;gt;
 &amp;lt;/head&amp;gt;
 &amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;Firebase Auth for Chrome Extension&amp;lt;/h1&amp;gt;
  &amp;lt;script type="module" src="signInWithPopup.js"&amp;gt;&amp;lt;/script&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;j)&lt;/strong&gt; Create a signInWithPopup.js file &lt;br&gt;
&lt;em&gt;firebase-project/public/signInWithPopup.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { initializeApp } from 'firebase/app';
import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';

const firebaseConfig = {
  // Your web app's Firebase configuration
  // Replace with the config you copied from Firebase Console
};

const app = initializeApp(firebaseConfig);
const auth = getAuth();

// This gives you a reference to the parent frame, i.e. the offscreen document.
const PARENT_FRAME = document.location.ancestorOrigins[0];

const PROVIDER = new GoogleAuthProvider();

function sendResponse(result) {
  window.parent.postMessage(JSON.stringify(result), PARENT_FRAME);
}

window.addEventListener('message', function({data}) {
  if (data.initAuth) {
    signInWithPopup(auth, PROVIDER)
      .then(sendResponse)
      .catch(sendResponse);
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;k)&lt;/strong&gt; Deploy the Firebase project&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g firebase-tools (You might need to run this as sudo)
firebase login
firebase init hosting (Use existing project which we created in the firebase console earlier)

What do you want to use as your public directory? public
Configure as a single-page app (rewrite all urls to /index.html)? Yes
Set up automatic builds and deploys with GitHub? No
File public/index.html already exists. Overwrite? No

firebase deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note the hosting URL provided after deployment. You'll need this for the Chrome extension.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Set up the Chrome Extension
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;a)&lt;/strong&gt; Navigate to the chrome-extension directory&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd ../chrome-extension
npm init -y
mkdir src
mkdir src/public
mkdir src/background
mkdir src/popup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;b)&lt;/strong&gt; We'll be using webpack to build our extension, you can install it with the following command along with firebase&lt;br&gt;
&lt;code&gt;npm i -D webpack webpack-cli html-webpack-plugin copy-webpack-plugin firebase&lt;/code&gt; and create a basic webpack config file &lt;br&gt;
&lt;em&gt;chrome-extension/webpack.config.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const path = require('path'),
 CopyWebpackPlugin = require('copy-webpack-plugin'),
 HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    background: './src/background/background.js',
    popup: './src/popup/popup.js',
  },
  mode: 'development',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src", "popup", "popup.html"),
      filename: "popup.html",
      chunks: ["firebase_config"]
    }),
    new CopyWebpackPlugin({
      patterns: [
        { from: './public/' }
      ],
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the package.json file, add the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
    "release": "webpack --config webpack.config.js &amp;amp;&amp;amp; zip -r dist.zip dist/*"
  },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;c)&lt;/strong&gt; Create a manifest.json file &lt;br&gt;
&lt;em&gt;chrome-extension/src/public/manifest.json&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "manifest_version": 3,
  "name": "Firebase Auth Extension",
  "version": "1.0",
  "description": "Chrome extension with Firebase Authentication",
  "permissions": [
    "identity",
    "storage",
    "offscreen"
  ],
  "host_permissions": [
    "https://*.firebaseapp.com/*"
  ],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "action": {
    "default_popup": "popup.html"
  },
  "web_accessible_resources": [
    {
      "resources": ["offscreen.html"],
      "matches": ["&amp;lt;all_urls&amp;gt;"]
    }
  ],
  "oauth2": {
    "client_id": "YOUR-GOOGLE-OAUTH-CLIENT-ID.apps.googleusercontent.com",
    "scopes": [
      "openid", 
      "email", 
      "profile"
    ]
  },
  "key": "-----BEGIN PUBLIC KEY-----\nYOURPUBLICKEY\n-----END PUBLIC KEY-----"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;d)&lt;/strong&gt; Create a popup.html file&lt;br&gt;
&lt;em&gt;chrome-extension/src/popup/popup.html&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Firebase Auth Extension&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Firebase Auth Extension&amp;lt;/h1&amp;gt;
    &amp;lt;div id="userInfo"&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;button id="signInButton"&amp;gt;Sign In&amp;lt;/button&amp;gt;
    &amp;lt;button id="signOutButton" style="display:none;"&amp;gt;Sign Out&amp;lt;/button&amp;gt;
    &amp;lt;script src="popup.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;e)&lt;/strong&gt; Create a popup.js file&lt;br&gt;
&lt;em&gt;chrome-extension/src/popup/popup.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.addEventListener('DOMContentLoaded', function() {
    const signInButton = document.getElementById('signInButton');
    const signOutButton = document.getElementById('signOutButton');
    const userInfo = document.getElementById('userInfo');

    function updateUI(user) {
        if (user) {
            userInfo.textContent = `Signed in as: ${user.email}`;
            signInButton.style.display = 'none';
            signOutButton.style.display = 'block';
        } else {
            userInfo.textContent = 'Not signed in';
            signInButton.style.display = 'block';
            signOutButton.style.display = 'none';
        }
    }

    chrome.storage.local.get(['user'], function(result) {
        updateUI(result.user);
    });

    signInButton.addEventListener('click', function() {
        chrome.runtime.sendMessage({action: 'signIn'}, function(response) {
            if (response.user) {
                updateUI(response.user);
            }
        });
    });

    signOutButton.addEventListener('click', function() {
        chrome.runtime.sendMessage({action: 'signOut'}, function() {
            updateUI(null);
        });
    });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;f)&lt;/strong&gt; Create a background.js file &lt;br&gt;
&lt;em&gt;chrome-extension/src/background/background.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const OFFSCREEN_DOCUMENT_PATH = 'offscreen.html';
const FIREBASE_HOSTING_URL = 'https://your-project-id.web.app'; // Replace with your Firebase hosting URL

let creatingOffscreenDocument;

async function hasOffscreenDocument() {
    const matchedClients = await clients.matchAll();
    return matchedClients.some((client) =&amp;gt; client.url.endsWith(OFFSCREEN_DOCUMENT_PATH));
}

async function setupOffscreenDocument() {
    if (await hasOffscreenDocument()) return;

    if (creatingOffscreenDocument) {
        await creatingOffscreenDocument;
    } else {
        creatingOffscreenDocument = chrome.offscreen.createDocument({
            url: OFFSCREEN_DOCUMENT_PATH,
            reasons: [chrome.offscreen.Reason.DOM_SCRAPING],
            justification: 'Firebase Authentication'
        });
        await creatingOffscreenDocument;
        creatingOffscreenDocument = null;
    }
}

async function getAuthFromOffscreen() {
    await setupOffscreenDocument();
    return new Promise((resolve, reject) =&amp;gt; {
        chrome.runtime.sendMessage({action: 'getAuth', target: 'offscreen'}, (response) =&amp;gt; {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve(response);
            }
        });
    });
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) =&amp;gt; {
    if (message.action === 'signIn') {
        getAuthFromOffscreen()
            .then(user =&amp;gt; {
                chrome.storage.local.set({user: user}, () =&amp;gt; {
                    sendResponse({user: user});
                });
            })
            .catch(error =&amp;gt; {
                console.error('Authentication error:', error);
                sendResponse({error: error.message});
            });
        return true; // Indicates we will send a response asynchronously
    } else if (message.action === 'signOut') {
        chrome.storage.local.remove('user', () =&amp;gt; {
            sendResponse();
        });
        return true;
    }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;g)&lt;/strong&gt; Create an offscreen.html file&lt;br&gt;
 &lt;em&gt;chrome-extension/src/public/offscreen.html&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Offscreen Document&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;script src="offscreen.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;h)&lt;/strong&gt; Create an offscreen.js file &lt;br&gt;
&lt;em&gt;chrome-extension/src/public/offscreen.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const FIREBASE_HOSTING_URL = 'https://your-project-id.web.app'; // Replace with your Firebase hosting URL

const iframe = document.createElement('iframe');
iframe.src = FIREBASE_HOSTING_URL;
document.body.appendChild(iframe);

chrome.runtime.onMessage.addListener((message, sender, sendResponse) =&amp;gt; {
    if (message.action === 'getAuth' &amp;amp;&amp;amp; message.target === 'offscreen') {
        function handleIframeMessage({data}) {
            try {
                const parsedData = JSON.parse(data);
                window.removeEventListener('message', handleIframeMessage);
                sendResponse(parsedData.user);
            } catch (e) {
                console.error('Error parsing iframe message:', e);
            }
        }

        window.addEventListener('message', handleIframeMessage);
        iframe.contentWindow.postMessage({initAuth: true}, FIREBASE_HOSTING_URL);
        return true; // Indicates we will send a response asynchronously
    }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Create an oauth ID in Google console
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;a)&lt;/strong&gt; Open the &lt;a href="https://console.developers.google.com/apis/credentials" rel="noopener noreferrer"&gt;Google Cloud console&lt;/a&gt; It should open on the Credentials page.&lt;br&gt;
&lt;strong&gt;b)&lt;/strong&gt; Click Create credentials &amp;gt; OAuth client ID.&lt;br&gt;
&lt;strong&gt;c)&lt;/strong&gt; Select the Chrome extension application type.&lt;br&gt;
&lt;strong&gt;d)&lt;/strong&gt; Name your OAuth 2.0 client and click Create. You may have to verify your app ownership.&lt;br&gt;
&lt;strong&gt;e)&lt;/strong&gt; Copy your Google OAUTH Client ID into the manifest.json where it says 'YOUR-GOOGLE-OAUTH-CLIENT-ID'&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Build and Upload your extension to the Chrome Web Store
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;a)&lt;/strong&gt; Sign up and pay the one time $5 fee if you want to create and publish extensions in the Chrome Web Store (If you don't do this then you won't be able to get a public key or Extension ID which are needed for this tutorial to work and you will only be able to run your chrome extensions locally)&lt;br&gt;
&lt;strong&gt;b)&lt;/strong&gt; Navigate to the Chrome Web Store Developer Dashboard, click on the Items menu option on the left hand side and then click the '+ New item' on the right side of the page&lt;br&gt;
&lt;strong&gt;c)&lt;/strong&gt; Build and zip your extension using the script we created earlier like so: &lt;code&gt;npm run release&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: I use webpack, you can use Vite or anything else you prefer)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;d)&lt;/strong&gt; Upload your Dist.zip file to the Chrome Web Store after clicking the new item button&lt;br&gt;
&lt;strong&gt;e)&lt;/strong&gt; Now you'll have a draft listing, you can fill in the info and save draft or complete it later.&lt;br&gt;
&lt;strong&gt;f)&lt;/strong&gt; You can find your Extension ID at the top of the page on the left side and to get your Public key, click on Package in the menu on the left and then click 'View public key'&lt;br&gt;
&lt;strong&gt;g)&lt;/strong&gt;  Copy your public key in between the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- lines into your manifest where it says 'YOURPUBLICKEY' make sure it's between the two '/n' statements&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Configure Firebase Authentication
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;a)&lt;/strong&gt; In the Firebase Console, go to Authentication &amp;gt; Sign-in method.&lt;br&gt;
&lt;strong&gt;b)&lt;/strong&gt; Enable Google as a sign-in provider.&lt;br&gt;
&lt;strong&gt;c)&lt;/strong&gt; Add your Chrome extension's ID ( Step 5 -&amp;gt; f) ) to the authorized domains list:&lt;br&gt;
The format is: chrome-extension://YOUR_EXTENSION_ID&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Load and Test the Extension
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;a)&lt;/strong&gt; Open Google Chrome and go to chrome://extensions/.&lt;br&gt;
&lt;strong&gt;b)&lt;/strong&gt; Enable "Developer mode" in the top right corner.&lt;br&gt;
&lt;strong&gt;c)&lt;/strong&gt; Click "Load unpacked" and select your chrome-extension directory.&lt;br&gt;
&lt;strong&gt;d)&lt;/strong&gt; Click on the extension icon in Chrome's toolbar to open the popup.&lt;br&gt;
&lt;strong&gt;e)&lt;/strong&gt; Click the "Sign In" button and test the authentication flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;p&gt;If you encounter CORS issues, ensure your Firebase hosting URL is correctly set in both background.js and offscreen.js.&lt;/p&gt;

&lt;p&gt;Make sure your Chrome extension's ID is correctly added to Firebase's authorized domains.&lt;/p&gt;

&lt;p&gt;Check the console logs in the popup, background script, and offscreen document for any error messages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;You now have a Chrome extension that uses Firebase Authentication with an offscreen document to handle the sign-in process. This setup allows for secure authentication without exposing sensitive Firebase configuration details directly in the extension code.&lt;/p&gt;

&lt;p&gt;Remember to replace placeholder values (like YOUR_EXTENSION_ID, YOUR-CLIENT-ID, YOUR_PUBLIC_KEY, and your-project-id) with your actual values before publishing your extension.&lt;/p&gt;

&lt;p&gt;If you found this guide helpful please give us a star and follow us on Dev.to for more guides&lt;br&gt;
&lt;a href="https://dev.to/lvn1"&gt;Dev.to&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>google</category>
    </item>
  </channel>
</rss>
