<?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: Diego Segura</title>
    <description>The latest articles on DEV Community by Diego Segura (@dieguezz).</description>
    <link>https://dev.to/dieguezz</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%2F388542%2F788d29f6-02d0-476c-96ea-2cfe42372e5c.jpeg</url>
      <title>DEV Community: Diego Segura</title>
      <link>https://dev.to/dieguezz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dieguezz"/>
    <language>en</language>
    <item>
      <title>I Built an Enterprise-Grade VPN with Terraform. It's Also My Ultimate "Life Hack" Tool.</title>
      <dc:creator>Diego Segura</dc:creator>
      <pubDate>Sat, 25 Oct 2025 01:44:33 +0000</pubDate>
      <link>https://dev.to/dieguezz/i-built-an-enterprise-grade-vpn-with-terraform-its-also-my-ultimate-life-hack-tool-3704</link>
      <guid>https://dev.to/dieguezz/i-built-an-enterprise-grade-vpn-with-terraform-its-also-my-ultimate-life-hack-tool-3704</guid>
      <description>&lt;p&gt;I originally created this project to solve a complex engineering problem: how to deploy a hardened, hybrid VPN (modern WireGuard® for users, classic IPsec for partners) on GCP, all managed as code. It's got a load balancer, health checks, a cost-saving scheduler, and zero secrets in git. It's built for production.&lt;/p&gt;

&lt;p&gt;You can read the original, more corporate-focused article &lt;a href="https://dev.to/dieguezz/how-to-deploy-a-hardened-firezone-wireguard-classic-ipsec-vpn-on-google-cloud-with-terraform-1o61"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But then I realized... a secure, high-performance VPN with a deploy-anywhere-in-the-world capability, managed by Terraform, is also the &lt;em&gt;perfect&lt;/em&gt; tool for solving common "power user" problems.&lt;/p&gt;

&lt;p&gt;Forget the slow, blacklisted servers of commercial VPNs. When you control the metal (or in this case, the VM), you control everything.&lt;/p&gt;

&lt;p&gt;In this tutorial, I'll walk you through how to deploy my &lt;a href="https://github.com/dieguezz/terraform-gcp-vpn" rel="noopener noreferrer"&gt;terraform-gcp-vpn&lt;/a&gt; project. I'll cover the core setup, and then I'll show you how to leverage this "enterprise-grade" stack for two of the most popular VPN use cases: &lt;strong&gt;geo-unblocking content&lt;/strong&gt; and &lt;strong&gt;saving money on subscriptions&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 What We're Building
&lt;/h2&gt;

&lt;p&gt;This isn't just a single VM running a script. This Terraform setup deploys a complete, resilient architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Firezone (WireGuard):&lt;/strong&gt; A fantastic open-source UI and management portal for WireGuard. This is for your personal devices (phone, laptop).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;strongSwan (IPsec):&lt;/strong&gt; (Optional) The classic, battle-tested IPsec server for site-to-site tunnels. You can probably ignore this for personal use, but it's there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP Load Balancer:&lt;/strong&gt; Provides a stable, static IP and automatic health checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP Cloud Scheduler:&lt;/strong&gt; This is a key component. It automatically starts and stops your VM on a schedule, so you're not paying for it 24/7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP Secret Manager:&lt;/strong&gt; All credentials (like IPsec PSKs) are stored securely, not in &lt;code&gt;.tfvars&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardened Security:&lt;/strong&gt; Follows OpenSSF Scorecard recommendations, uses least-privilege IAM, and has a strict firewall.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This is a tutorial for a technical audience. You'll need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; A &lt;strong&gt;GCP Project&lt;/strong&gt; with billing enabled.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Terraform v1.9+&lt;/strong&gt; installed locally.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;gcloud CLI&lt;/strong&gt; authenticated (&lt;code&gt;gcloud auth application-default login&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; A &lt;strong&gt;Service Account&lt;/strong&gt; with the necessary roles (e.g., &lt;code&gt;roles/compute.instanceAdmin.v1&lt;/code&gt;, &lt;code&gt;roles/cloudscheduler.admin&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; A domain name (e.g., &lt;code&gt;vpn.yourdomain.com&lt;/code&gt;) you can point to the new IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🛠️ The Tutorial: Deploying Your Personal VPN Hub
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Clone and Configure
&lt;/h3&gt;

&lt;p&gt;First, get the code and create your configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="o"&gt;[&lt;/span&gt;https://github.com/dieguezz/terraform-gcp-vpn.git]&lt;span class="o"&gt;(&lt;/span&gt;https://github.com/dieguezz/terraform-gcp-vpn.git&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;terraform-gcp-vpn
&lt;span class="nb"&gt;cp &lt;/span&gt;terraform.tfvars.example terraform.tfvars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, open terraform.tfvars in your editor. This is where the magic happens. Let's look at the key variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# terraform.tfvars&lt;/span&gt;

&lt;span class="nx"&gt;project_id&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-gcp-project-id"&lt;/span&gt;
&lt;span class="nx"&gt;region&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-central1"&lt;/span&gt;
&lt;span class="nx"&gt;zone&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-central1-a"&lt;/span&gt;

&lt;span class="c1"&gt;# --- Network ---&lt;/span&gt;
&lt;span class="nx"&gt;network_name&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpn-network"&lt;/span&gt;
&lt;span class="nx"&gt;subnet_name&lt;/span&gt;  &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpn-subnet"&lt;/span&gt;
&lt;span class="nx"&gt;subnet_cidr&lt;/span&gt;  &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.128.0.0/20"&lt;/span&gt;

&lt;span class="c1"&gt;# --- Firezone ---&lt;/span&gt;
&lt;span class="nx"&gt;firezone_hostname&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpn.yourdomain.com"&lt;/span&gt;
&lt;span class="c1"&gt;# ... other firezone vars like admin email&lt;/span&gt;

&lt;span class="c1"&gt;# --- Scheduling ---&lt;/span&gt;
&lt;span class="nx"&gt;enable_scheduling&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;start_schedule&lt;/span&gt;    &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0 6 * * 1-5"&lt;/span&gt;  &lt;span class="c1"&gt;# 6am Mon-Fri&lt;/span&gt;
&lt;span class="nx"&gt;stop_schedule&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0 23 * * 1-5"&lt;/span&gt; &lt;span class="c1"&gt;# 11pm Mon-Fri&lt;/span&gt;
&lt;span class="nx"&gt;timezone&lt;/span&gt;          &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"America/New_York"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: The "Hacks" Are Just Variables
&lt;/h3&gt;

&lt;p&gt;This is where this project gets powerful.&lt;/p&gt;

&lt;p&gt;👉 For Geo-Unblocking (e.g., US Netflix)&lt;br&gt;
Commercial VPNs get their IPs blocked by streaming services constantly. Your personal, fresh IP from GCP? Much less likely.&lt;/p&gt;

&lt;p&gt;Want to access the US Netflix or Hulu library? Just set your region to a US-based one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;region = "us-central1" # or us-east1, us-west1, etc.
zone   = "us-central1-a"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want to access BBC iPlayer?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;region = "europe-west2" # London
zone   = "europe-west2-a"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the idea. You can deploy your VPN endpoint anywhere GCP has a presence.&lt;/p&gt;

&lt;p&gt;👉 For Subscription Savings (e.g., YouTube Premium)&lt;br&gt;
Services like YouTube Premium or Spotify often have different pricing based on your country. (Disclaimer: This may be against their ToS, so proceed at your own risk).&lt;/p&gt;

&lt;p&gt;Want to see if you can get a better price from another country? Just deploy your VPN there. For example, some users report savings by connecting from regions in South America or Asia.&lt;/p&gt;

&lt;p&gt;The power of Terraform means you can terraform destroy this instance and spin one up in us-central1 30 seconds later. You have a global fleet of personal VPNs at your fingertips.&lt;/p&gt;

&lt;p&gt;👉 For Saving Money (The Scheduler)&lt;br&gt;
A production-grade setup like this isn't free (see cost breakdown below). The e2-medium instance and the Load Balancer are the main costs.&lt;/p&gt;

&lt;p&gt;But do you really need your streaming VPN running at 3 AM on a Tuesday?&lt;/p&gt;

&lt;p&gt;By setting &lt;code&gt;enable_scheduling = true&lt;/code&gt;, you can configure the VM to only run when you'll actually use it. For example, to only run it on evenings and weekends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enable_scheduling = true
start_schedule    = "0 18 * * 1-5" # 6 PM on Weekdays
stop_schedule     = "0 23 * * 1-5" # 11 PM on Weekdays
# Note: You'd need more rules for weekends, or just run it manually.
timezone          = "Europe/Madrid"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single variable can cut your VM costs by 50-80%, turning an expensive setup into something that's a few bucks a month.&lt;/p&gt;

&lt;p&gt;Or, my favorite method:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;enable_scheduling = false&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;...and I just manually run terraform apply when I want to stream, and terraform destroy when I'm done. The entire deployment takes about 5 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Deploy!
&lt;/h3&gt;

&lt;p&gt;Once your terraform.tfvars file is saved, the rest is standard Terraform.&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;# Initialize the modules&lt;/span&gt;
terraform init

&lt;span class="c"&gt;# See what's about to be built&lt;/span&gt;
terraform plan

&lt;span class="c"&gt;# Deploy the infrastructure&lt;/span&gt;
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type yes when prompted. Go grab a coffee. In about 5 minutes, Terraform will finish and spit out the outputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outputs:

firezone_url     = "[https://vpn.yourdomain.com](https://vpn.yourdomain.com)"
load_balancer_ip = "35.xxx.xxx.xxx"
vpn_instance_name = "firezone-vpn-instance"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Access Your VPN Hub
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Point Your DNS: Go to your DNS provider and create an A record for vpn.yourdomain.com (or whatever you set as firezone_hostname) pointing to the load_balancer_ip.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wait: Give Firezone 2-3 minutes to initialize on the first boot. You can check the logs with gcloud compute instances get-serial-port-output firezone-vpn-instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Login: Open &lt;a href="https://vpn.yourdomain.com" rel="noopener noreferrer"&gt;https://vpn.yourdomain.com&lt;/a&gt; in your browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enroll Devices: Use the Firezone UI to add your users (just you, probably) and enroll your devices. You can download the WireGuard config or scan a QR code with the mobile app.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. You are now connected to the internet through your own private, secure, high-speed server.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔒 A Note on Security and Privacy
&lt;/h2&gt;

&lt;p&gt;The #1 reason to do this is privacy.&lt;/p&gt;

&lt;p&gt;Zero Logs: Is your commercial VPN really "zero-log"? You have to trust them. Here, you own the server. You know there are no logs because you'd have to be the one to set them up.&lt;/p&gt;

&lt;p&gt;Hardened: This deployment follows OpenSSF Scorecard recommendations. No secrets in code, pinned dependencies, encrypted boot disk, and least-privilege IAM.&lt;/p&gt;

&lt;p&gt;Public Wi-Fi: Using this on a coffee shop or airport Wi-Fi means your traffic is encrypted inside a WireGuard tunnel to your server. No one on the local network can snoop on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building your own VPN used to be a pain. With tools like Terraform and Firezone, it's now a 5-minute process.&lt;/p&gt;

&lt;p&gt;While I built this repo for corporate use, its real power comes from its flexibility. You get an enterprise-grade stack that you can use to stream Netflix, save money on YouTube, or simply secure your traffic at a coffee shop—all with a few changes to a .tfvars file.&lt;/p&gt;

&lt;p&gt;You're a developer. Stop renting your infrastructure. Own it.&lt;/p&gt;

&lt;p&gt;If this helped you, I'd appreciate a ⭐ on the GitHub repo!&lt;/p&gt;

&lt;p&gt;GitHub Repo: &lt;a href="https://github.com/dieguezz/terraform-gcp-vpn" rel="noopener noreferrer"&gt;dieguezz/terraform-gcp-vpn&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Deploy a Hardened Firezone (WireGuard) + Classic IPsec VPN on Google Cloud with Terraform 🚀</title>
      <dc:creator>Diego Segura</dc:creator>
      <pubDate>Wed, 22 Oct 2025 02:23:03 +0000</pubDate>
      <link>https://dev.to/dieguezz/how-to-deploy-a-hardened-firezone-wireguard-classic-ipsec-vpn-on-google-cloud-with-terraform-1o61</link>
      <guid>https://dev.to/dieguezz/how-to-deploy-a-hardened-firezone-wireguard-classic-ipsec-vpn-on-google-cloud-with-terraform-1o61</guid>
      <description>&lt;h2&gt;
  
  
  Why This Baseline Helps 💡
&lt;/h2&gt;

&lt;p&gt;If you're managing remote access for a distributed team &lt;em&gt;and&lt;/em&gt; need site-to-site connectivity with partners running legacy IPsec, you've probably felt the pain of maintaining two separate VPN stacks. One modern (WireGuard via Firezone), one classic (strongSwan/Libreswan), both fighting for the same public IP and firewall rules.&lt;/p&gt;

&lt;p&gt;This guide walks you through a &lt;strong&gt;single Terraform deployment&lt;/strong&gt; that gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two access patterns, one deployment:&lt;/strong&gt; WireGuard for your remote users, Classic IPsec for partner networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero secrets in git:&lt;/strong&gt; All credentials live in GCP Secret Manager, referenced via data sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-ready from day one:&lt;/strong&gt; Load balancer health checks, automated backups, OpenSSF Scorecard hardening, and cost-saving VM schedulers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you'll have a working VPN gateway on GCP that your team can connect to via the Firezone mobile/desktop clients, while your partners connect via standard IPsec tunnels—all managed as code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You're Building 🏗️
&lt;/h2&gt;

&lt;p&gt;Here's the architecture at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Key Features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firezone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Modern WireGuard VPN with web UI&lt;/td&gt;
&lt;td&gt;User management, device enrollment, audit logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;strongSwan&lt;/strong&gt; (optional)&lt;/td&gt;
&lt;td&gt;Classic IPsec for site-to-site tunnels&lt;/td&gt;
&lt;td&gt;Partner compatibility, IKEv2/ESP support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GCP Load Balancer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Health-checked traffic routing&lt;/td&gt;
&lt;td&gt;Automatic failover, regional redundancy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Scheduler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cost optimization&lt;/td&gt;
&lt;td&gt;Auto-start 6am, auto-stop 11pm (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secret Manager&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Credential storage&lt;/td&gt;
&lt;td&gt;Zero plaintext secrets in code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Prerequisites Checklist ✅
&lt;/h2&gt;

&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GCP Project&lt;/strong&gt; with billing enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; v1.9+ installed locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gcloud CLI&lt;/strong&gt; authenticated (&lt;code&gt;gcloud auth application-default login&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Account&lt;/strong&gt; with roles:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;roles/compute.instanceAdmin.v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;roles/cloudscheduler.admin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you plan to enable site-to-site VPN tunnels, you'll also need to create secrets in GCP Secret Manager for the IPsec pre-shared keys (format: &lt;code&gt;vpn-psk-{tunnel_name}&lt;/code&gt;). See the &lt;a href="https://github.com/dieguezz/terraform-gcp-vpn/tree/main/modules/vpn-gateway" rel="noopener noreferrer"&gt;VPN Gateway module documentation&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 1: Clone and Configure 📦
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/dieguezz/terraform-gcp-vpn.git
&lt;span class="nb"&gt;cd &lt;/span&gt;terraform-gcp-vpn
&lt;span class="nb"&gt;cp &lt;/span&gt;terraform.tfvars.example terraform.tfvars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;terraform.tfvars&lt;/code&gt; with your values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-gcp-project-id"&lt;/span&gt;
&lt;span class="nx"&gt;region&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-central1"&lt;/span&gt;
&lt;span class="nx"&gt;zone&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-central1-a"&lt;/span&gt;

&lt;span class="c1"&gt;# Network&lt;/span&gt;
&lt;span class="nx"&gt;network_name&lt;/span&gt;    &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpn-network"&lt;/span&gt;
&lt;span class="nx"&gt;subnet_name&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpn-subnet"&lt;/span&gt;
&lt;span class="nx"&gt;subnet_cidr&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.128.0.0/20"&lt;/span&gt;

&lt;span class="c1"&gt;# Firezone&lt;/span&gt;
&lt;span class="nx"&gt;firezone_hostname&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpn.yourdomain.com"&lt;/span&gt;

&lt;span class="c1"&gt;# Scheduling (optional, remove to run 24/7)&lt;/span&gt;
&lt;span class="nx"&gt;enable_scheduling&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;start_schedule&lt;/span&gt;    &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0 6 * * 1-5"&lt;/span&gt;  &lt;span class="c1"&gt;# 6am Mon-Fri&lt;/span&gt;
&lt;span class="nx"&gt;stop_schedule&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0 23 * * 1-5"&lt;/span&gt; &lt;span class="c1"&gt;# 11pm Mon-Fri&lt;/span&gt;
&lt;span class="nx"&gt;timezone&lt;/span&gt;          &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"America/New_York"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Initialize Terraform 🔧
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see modules downloading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initializing modules...
- compute in modules/compute
- load_balancer in modules/load-balancer
- network in modules/network
- scheduler in modules/scheduler
- security in modules/security
- vpn_gateway in modules/vpn-gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Review the Plan 👀
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will show you what's about to be created. Pay attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute Engine instance&lt;/strong&gt; (e2-medium with 50GB boot disk)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC network and subnet&lt;/strong&gt; with firewall rules for UDP 51820, 500, 4500&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancer&lt;/strong&gt; with health check on Firezone port 13000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Functions&lt;/strong&gt; for start/stop scheduling (if &lt;code&gt;enable_scheduling = true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM bindings&lt;/strong&gt; for Secret Manager access&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 4: Deploy 🚢
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;yes&lt;/code&gt; when prompted. The deployment takes ~5 minutes. Coffee break time ☕&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Access Firezone 🌐
&lt;/h2&gt;

&lt;p&gt;Once complete, Terraform outputs the load balancer IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outputs:

firezone_url = "https://35.xxx.xxx.xxx"
load_balancer_ip = "35.xxx.xxx.xxx"
vpn_instance_name = "firezone-vpn-instance"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Point your DNS:&lt;/strong&gt; Create an A record for &lt;code&gt;vpn.yourdomain.com&lt;/code&gt; → &lt;code&gt;35.xxx.xxx.xxx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait 2-3 minutes&lt;/strong&gt; for Firezone to initialize (check startup script logs with &lt;code&gt;gcloud compute instances get-serial-port-output&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open browser:&lt;/strong&gt; Navigate to &lt;code&gt;https://vpn.yourdomain.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login:&lt;/strong&gt; Use your Google Workspace credentials (configured via &lt;code&gt;firezone_admin_email&lt;/code&gt; and &lt;code&gt;google_workspace_domain&lt;/code&gt; variables)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enroll devices:&lt;/strong&gt; Add users, generate WireGuard configs via the Firezone UI&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 6: Site-to-Site VPN (Optional) 🔗
&lt;/h2&gt;

&lt;p&gt;This deployment also supports &lt;strong&gt;Classic IPsec tunnels&lt;/strong&gt; for connecting with partner networks or legacy infrastructure. This is completely optional and disabled by default.&lt;/p&gt;

&lt;p&gt;If you need this capability, the infrastructure includes a pre-configured strongSwan setup. To enable it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;enable_site_to_site_vpn = true&lt;/code&gt; in your &lt;code&gt;terraform.tfvars&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Configure your tunnel parameters in the &lt;code&gt;vpn_tunnels&lt;/code&gt; variable&lt;/li&gt;
&lt;li&gt;Create the required pre-shared keys in Secret Manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For detailed configuration steps, see the &lt;strong&gt;&lt;a href="https://github.com/dieguezz/terraform-gcp-vpn/tree/main/modules/vpn-gateway" rel="noopener noreferrer"&gt;VPN Gateway Module Documentation&lt;/a&gt;&lt;/strong&gt; which includes complete examples and troubleshooting guides.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Hardening Applied 🔒
&lt;/h2&gt;

&lt;p&gt;This deployment follows OpenSSF Scorecard recommendations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;No secrets in code&lt;/strong&gt; – all credentials in Secret Manager&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Pinned dependencies&lt;/strong&gt; – GitHub Actions use SHA hashes&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;SAST enabled&lt;/strong&gt; – CodeQL scans on every PR&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Least-privilege IAM&lt;/strong&gt; – service account has minimal roles&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Encrypted boot disk&lt;/strong&gt; – GCP default encryption at rest&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Regular backups&lt;/strong&gt; – VM snapshots via Cloud Scheduler (roadmap item)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cost Breakdown (Monthly Estimate) 💰
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Cost (USD/month)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute Engine&lt;/td&gt;
&lt;td&gt;e2-medium (50% usage with scheduler)&lt;/td&gt;
&lt;td&gt;~$12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Balancer&lt;/td&gt;
&lt;td&gt;Network LB with health checks&lt;/td&gt;
&lt;td&gt;~$18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC&lt;/td&gt;
&lt;td&gt;Egress traffic (10GB assumed)&lt;/td&gt;
&lt;td&gt;~$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret Manager&lt;/td&gt;
&lt;td&gt;4 secrets, 10 accesses/day&lt;/td&gt;
&lt;td&gt;&amp;lt;$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Scheduler&lt;/td&gt;
&lt;td&gt;2 jobs (start/stop)&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$32/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Savings:&lt;/strong&gt; Without the scheduler (24/7 runtime), the e2-medium costs ~$24/month. The scheduler saves you ~$12/month (50% reduction).&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;terraform apply&lt;/code&gt; fails with "secret not found"&lt;/td&gt;
&lt;td&gt;VPN tunnel enabled but PSK secret missing&lt;/td&gt;
&lt;td&gt;Create secret: &lt;code&gt;gcloud secrets create vpn-psk-{tunnel_name}&lt;/code&gt; or disable site-to-site VPN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't access Firezone UI after 5 min&lt;/td&gt;
&lt;td&gt;Startup script still running&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;gcloud compute instances get-serial-port-output&lt;/code&gt; for errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WireGuard clients can't connect&lt;/td&gt;
&lt;td&gt;Firewall rules not applied&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;gcloud compute firewall-rules list&lt;/code&gt; includes &lt;code&gt;allow-wireguard&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Workspace SSO not working&lt;/td&gt;
&lt;td&gt;Domain mismatch or OAuth misconfigured&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;google_workspace_domain&lt;/code&gt; matches your workspace and check Firezone OAuth settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instance stops unexpectedly&lt;/td&gt;
&lt;td&gt;Scheduler enabled but wrong timezone&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;timezone&lt;/code&gt; variable in &lt;code&gt;terraform.tfvars&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next? 🎯
&lt;/h2&gt;

&lt;p&gt;Now that you have a working baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add users:&lt;/strong&gt; Use the Firezone web UI to invite team members and generate device configs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor traffic:&lt;/strong&gt; Enable VPC Flow Logs (&lt;code&gt;gcloud compute networks subnets update --enable-flow-logs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate backups:&lt;/strong&gt; Extend the &lt;code&gt;scheduler&lt;/code&gt; module to snapshot the boot disk weekly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-region failover:&lt;/strong&gt; Deploy a second instance in another region, use Cloud DNS routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs:&lt;/strong&gt; Forward Firezone logs to Cloud Logging for compliance&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways 📝
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid VPN stacks don't have to be messy.&lt;/strong&gt; One Terraform deployment can handle both modern WireGuard and legacy IPsec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero secrets in git is achievable.&lt;/strong&gt; GCP Secret Manager + Terraform data sources = clean commit history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost control matters.&lt;/strong&gt; A simple scheduler can cut your VM bill in half for dev/staging environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security is a journey.&lt;/strong&gt; OpenSSF Scorecard gives you a roadmap—automated fixes get you 80% of the way.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Repository &amp;amp; Resources 📚
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/dieguezz/terraform-gcp-vpn" rel="noopener noreferrer"&gt;dieguezz/terraform-gcp-vpn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firezone Docs:&lt;/strong&gt; &lt;a href="https://www.firezone.dev/docs" rel="noopener noreferrer"&gt;firezone.dev/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;strongSwan Wiki:&lt;/strong&gt; &lt;a href="https://www.strongswan.org/" rel="noopener noreferrer"&gt;strongswan.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSSF Scorecard:&lt;/strong&gt; &lt;a href="https://securityscorecards.dev/" rel="noopener noreferrer"&gt;securityscorecards.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Got questions or want to share your VPN war stories? Drop a comment below or open an issue on GitHub. If this helped you, a ⭐ on the repo keeps me caffeinated for the next guide!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next in series:&lt;/strong&gt; &lt;em&gt;Architecture Deep Dive – How the Modules Talk to Each Other (and Why It Matters)&lt;/em&gt; 🏛️&lt;/p&gt;

</description>
      <category>security</category>
      <category>terraform</category>
      <category>devops</category>
      <category>firezone</category>
    </item>
    <item>
      <title>Creating a very simple blog with Next.js and Strapi</title>
      <dc:creator>Diego Segura</dc:creator>
      <pubDate>Mon, 07 Sep 2020 01:04:46 +0000</pubDate>
      <link>https://dev.to/dieguezz/creating-a-very-simple-blog-with-next-js-and-strapi-3pgk</link>
      <guid>https://dev.to/dieguezz/creating-a-very-simple-blog-with-next-js-and-strapi-3pgk</guid>
      <description>&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Setup Strapi locally and deploy it to heroku&lt;/li&gt;
&lt;li&gt;Data fetching strategies with Next.js&lt;/li&gt;
&lt;li&gt;Create dynamic routes &lt;/li&gt;
&lt;li&gt;Basic GraphQl&lt;/li&gt;
&lt;li&gt;Static SSR&lt;/li&gt;
&lt;li&gt;Deploy frontend to vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you need first
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Github account&lt;br&gt;
&lt;a href="https://github.com/join"&gt;Create a github account&lt;/a&gt; and &lt;a href="https://docs.github.com/es/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent"&gt;generate a new ssh key&lt;/a&gt;. Then &lt;a href="https://docs.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account"&gt;add it to your github account&lt;/a&gt; and you are ready.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Code editor&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Heroku account&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://signup.heroku.com/"&gt;Create an account&lt;/a&gt; if you don't have one&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://strapi.io/documentation/v3.x/deployment/heroku.html"&gt;Install heroku CLI&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://nodejs.org/es/"&gt;nodejs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;postgresql &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I I &lt;a href="https://wiki.archlinux.org/index.php/PostgreSQL"&gt;installed&lt;/a&gt; it for Arch but you can google how to install it for your env&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering strategy
&lt;/h2&gt;

&lt;p&gt;Check &lt;a href="https://developers.google.com/web/updates/2019/02/rendering-on-the-web"&gt;this&lt;/a&gt; out.&lt;/p&gt;

&lt;p&gt;On this article i will focus on Static SSR strategy for this blog. I like it because it will fit in 90% use cases and i think its really performant.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://strapi.io/documentation/v3.x/deployment/heroku.html#_1-heroku-cli-installation"&gt;Install Strapi with CLI&lt;/a&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Login to Heroku from your CLI
&lt;/h3&gt;

&lt;p&gt;Next, you need to login to Heroku from your computer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroku login
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Follow the instructions and return to your command line.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a &lt;a href="https://strapi.io/documentation/v3.x/getting-started/quick-start.html"&gt;new Strapi project&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn create strapi-app strapi-blog
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;When prompted, answer, on this order: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom&lt;/li&gt;
&lt;li&gt;postgres&lt;/li&gt;
&lt;li&gt;my-blog-db&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;a href="https://strapi.io/documentation/v3.x/deployment/heroku.html#_5-init-a-git-repository-and-commit-your-project"&gt;Init a Git repository and commit your project&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Init the Git repository&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd my-project
git init
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Add heroku remote&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroku git:remote -a blog-strapi-app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Your local development environment is now set-up and configured to work with Heroku. You have a new Strapi project and a new Heroku app ready to be configured to work with a database and with each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;a href="https://strapi.io/documentation/v3.x/deployment/heroku.html#_7-heroku-database-set-up"&gt;Heroku Database set-up&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroku addons:create heroku-postgresql:hobby-dev
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Database credentials
&lt;/h3&gt;

&lt;p&gt;The add-on automatically exposes the database credentials into a single environment variable accessible by your app. To retrieve it, type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroku config
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This should print something like this: &lt;code&gt;DATABASE_URL: postgres://ebitxebvixeeqd:dc59b16dedb3a1eef84d4999sb4baf@ec2-50-37-231-192.compute-2.amazonaws.com: 5432/d516fp1u21ph7b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;(This url is read like so: postgres:// &lt;strong&gt;USERNAME&lt;/strong&gt;  : &lt;strong&gt;PASSWORD&lt;/strong&gt; @ &lt;strong&gt;HOST&lt;/strong&gt; : &lt;strong&gt;PORT&lt;/strong&gt; : &lt;strong&gt;DATABASE_NAME&lt;/strong&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  6. &lt;a href="https://strapi.io/documentation/v3.x/deployment/heroku.html#_3-set-environment-variables"&gt;Set environment variables&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Strapi expects a variable for each database connection configuration (host, username, etc.). So, from the url above, you have to set several environment variables in the Heroku config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroku config:set DATABASE_USERNAME=ebitxebvixeeqd
heroku config:set DATABASE_PASSWORD=dc59b16dedb3a1eef84d4999a0be041bd419c474cd4a0973efc7c9339afb4baf
heroku config:set DATABASE_HOST=ec2-50-37-231-192.compute-2.amazonaws.com
heroku config:set DATABASE_PORT=5432
heroku config:set DATABASE_NAME=d516fp1u21ph7b
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Replace these above values with your actual values.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. &lt;a href="https://strapi.io/documentation/v3.x/deployment/heroku.html#_4-update-your-database-config-file"&gt;Update your database config file&lt;/a&gt;.
&lt;/h3&gt;

&lt;p&gt;Replace the contents of &lt;code&gt;config/database.js&lt;/code&gt; with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = ({ env }) =&amp;gt; ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'bookshelf',
      settings: {
        client: 'postgres',
        host: env('DATABASE_HOST', '127.0.0.1'),
        port: env.int('DATABASE_PORT', 27017),
        database: env('DATABASE_NAME', 'my-blog-db'),
        username: env('DATABASE_USERNAME', ''),
        password: env('DATABASE_PASSWORD', ''),
      },
      options: {
        ssl: false,
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  8. &lt;a href="https://strapi.io/documentation/v3.x/deployment/heroku.html#_9-deploy"&gt;Deploy&lt;/a&gt;.
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add --all
git commit -m "update config"
git push heroku master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The deployment may take a few minutes. At the end, logs will display the url of your project (e.g. &lt;code&gt;https://mighty-taiga-80884.herokuapp.com&lt;/code&gt;). You can also open your project using the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroku open
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;If you see the Strapi Welcome page, you have correctly set-up, configured and deployed your Strapi project on Heroku. You will now need to set-up your &lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create our Next.js app
&lt;/h3&gt;

&lt;p&gt;We move to the parent folder as we dont want to mix strapi and frontend files and we create our app&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd ..
yarn create next-app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Routing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;We create a file &lt;code&gt;pages/article/[id].js&lt;/code&gt; which will be our article item page:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Head from "next/head";

export default function ArticlePage() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;title&amp;gt;This is an article title&amp;lt;/title&amp;gt;
      &amp;lt;/Head&amp;gt;
      &amp;lt;main&amp;gt;
        This is an article body
      &amp;lt;/main&amp;gt;

    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;We empty the contents of &lt;code&gt;pages/index.js&lt;/code&gt; as its our home page and it will contiain our &lt;code&gt;ArticleList&lt;/code&gt; in the future.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import styles from "../styles/Home.module.scss";

export default function Home() {
  return (
    &amp;lt;div className={styles.container}&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;title&amp;gt;Our great blog&amp;lt;/title&amp;gt;
        &amp;lt;link rel="icon" href="/favicon.ico" /&amp;gt;
      &amp;lt;/Head&amp;gt;

      &amp;lt;main className={styles.main}&amp;gt;
        The article list
      &amp;lt;/main&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;We create a 404 custom page by adding a &lt;code&gt;pages/404.js&lt;/code&gt; with a PageNotFound component.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default function PageNotFound() {
    return &amp;lt;div&amp;gt;oh oh...&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Create some structure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a &lt;code&gt;modules&lt;/code&gt; folder at the root of our Next.js project&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;modules/core&lt;/code&gt; module with two subfolders &lt;code&gt;components&lt;/code&gt; and &lt;code&gt;services&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;modules/core/components&lt;/code&gt; create a &lt;code&gt;Header.js&lt;/code&gt; and a &lt;code&gt;Footer.js&lt;/code&gt; files like:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Link from "next/link";

export default function Header() {
  return (
    &amp;lt;header&amp;gt;
      &amp;lt;Link href="/"&amp;gt;
        &amp;lt;a&amp;gt;Our awesome blog&amp;lt;/a&amp;gt;
      &amp;lt;/Link&amp;gt;
    &amp;lt;/header&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;





&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default function Footer() {
  return &amp;lt;footer&amp;gt;® Awesome friends company&amp;lt;/footer&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Consume your components in &lt;code&gt;app.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "../styles/globals.scss";
import Header from "../modules/core/components/Header";
import Footer from "../modules/core/components/Footer";

function MyApp({ Component, pageProps }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Header&amp;gt;&amp;lt;/Header&amp;gt;
      &amp;lt;div className="route"&amp;gt;
        &amp;lt;Component {...pageProps} /&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;Footer&amp;gt;&amp;lt;/Footer&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default MyApp;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Remove the folder &lt;code&gt;routes/api&lt;/code&gt; as we are not using microservices for our example, it will be fully static.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install SASS (optional)&lt;br&gt;
We are using sass because we want to be able of reusing variables for colors.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add sass
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Change all .css files to .scss (omit if you did not add sass)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a reset.scss file next to global.scss with your &lt;a href="https://gist.github.com/DavidWells/18e73022e723037a50d6"&gt;favourite css reset&lt;/a&gt;. (use .css if you didn't add sass)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Empty the contents of global.scss and import reset.scss like:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@import 'reset'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a file &lt;code&gt;variables.scss&lt;/code&gt; next to &lt;code&gt;global.scss&lt;/code&gt; and define your colors, breakpoints or whatever variables you need to share here. For our case, i picked a random palette from &lt;a href="https://coolors.co/"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;variables.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$charcoal: #264653;
$persianGreen: #2A9D8F;
$orangeYellowCrayola: #E9C46A;
$sandyBrown: #F4A261;
$burntSienna: #E76F51;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Empty the contents of &lt;code&gt;Home.module.scss&lt;/code&gt; to leave it like
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.container {
  display: block;
}

.main {
  display: block;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;You can style it from here in the future.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data fetching
&lt;/h3&gt;

&lt;p&gt;Launch your strapi in local. For this, go to the folder where you installed strapi and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn develop
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Log into your admin account in &lt;code&gt;http://localhost:1337/admin&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;create an &lt;code&gt;article&lt;/code&gt; content type and add the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canonical (will be our permalink or pathname)&lt;/li&gt;
&lt;li&gt;description (long text)&lt;/li&gt;
&lt;li&gt;title (short text)&lt;/li&gt;
&lt;li&gt;body (rich text)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;save everything and open the strapi folder in your text editor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a schema.graphql.js inside &lt;code&gt;api/article/config&lt;/code&gt; with the following:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { sanitizeEntity } = require('strapi-utils');

module.exports = {
  query: `
    articleByCanonical(canonical: ID): Article
  `,
  resolver: {
    Query: {
      articleByCanonical: {
        resolverOf: 'Article.findOne',
        async resolver(_, { canonical }) {
          const entity = await strapi.services.article.findOne({ canonical });
          return sanitizeEntity(entity, { model: strapi.models.article });
        },
      },
    },
  },
};
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And save everything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Now, on your frontend project add the apollo client dependency (remember to &lt;code&gt;cd frontend dir&lt;/code&gt; to install the dependency in the frontend package.json)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @apollo/client
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a .env.local file with:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STRAPI_HOST="http://localhost:1337"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a service inside &lt;code&gt;core/services/apolloClient.service.js&lt;/code&gt; where we will export our apollo client instance.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { ApolloClient, InMemoryCache } from '@apollo/client'

export default new ApolloClient({
    uri: `${process.env.STRAPI_HOST}/graphql`,
    cache: new InMemoryCache()
})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a new module &lt;code&gt;article&lt;/code&gt; with &lt;code&gt;components&lt;/code&gt; and &lt;code&gt;services&lt;/code&gt; folders as we did with core.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create &lt;code&gt;ArticleList&lt;/code&gt; and &lt;code&gt;Article&lt;/code&gt; components inside &lt;code&gt;article/components/ArticleList.js&lt;/code&gt; and &lt;code&gt;article/components/Article.js&lt;/code&gt; with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default function Article({ title, body }) {
  return &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;
    &amp;lt;div&amp;gt;{body}&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;;
}

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Link from "next/link";

export default function ArticleList({ articles }) {
  return articles.length ? (
    &amp;lt;ol&amp;gt;
      {articles.map((article) =&amp;gt; (
        &amp;lt;li key={article.id}&amp;gt;
          &amp;lt;Link href={`article/${article.canonical}`}&amp;gt;
            &amp;lt;h2&amp;gt;{article.title}&amp;lt;/h2&amp;gt;
          &amp;lt;/Link&amp;gt;
          &amp;lt;p&amp;gt;{article.description}...&amp;lt;/p&amp;gt;
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ol&amp;gt;
  ) : (
    &amp;lt;div&amp;gt;There are no articles&amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create the articles service in &lt;code&gt;article/services/article.service.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { gql } from "@apollo/client";
import apolloClient from "../../core/services/apolloClient.service";

export async function getAll() {
  return apolloClient
    .query({
      query: gql`
        {
          articles {
            canonical
            description
            id
            created_at
            title
          }
        }
      `,
    })
    .then((result) =&amp;gt; result.data.articles);
}

export async function getByCanonical(canonical) {
  return apolloClient
    .query({
      query: gql`
        {
          articleByCanonical(canonical: "${canonical}") {
            title
            body
          }
        }
      `,
    })
    .then((result) =&amp;gt; {
      console.log(result.data)
      return result.data.articleByCanonical;
    });
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Finally, we add the fetching logic inside our routes and we consume these components:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;pages/index.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Head from "next/head";
import ArticleList from "../modules/article/components/ArticleList";
import { getAll } from "../modules/article/services/article.service";

export default function HomePage({ articles }) {
  return (
    &amp;lt;div className={styles.container}&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;title&amp;gt;Our great blog&amp;lt;/title&amp;gt;
        &amp;lt;link rel="icon" href="/favicon.ico" /&amp;gt;
      &amp;lt;/Head&amp;gt;

      &amp;lt;main className={styles.main}&amp;gt;
        &amp;lt;ArticleList articles={articles}&amp;gt;&amp;lt;/ArticleList&amp;gt;
      &amp;lt;/main&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export async function getStaticProps(context) {
  return {
    props: { articles: await getAll() },
  };
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And change &lt;code&gt;pages/article/[id].js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import marked from "marked";

import Article from "../../modules/article/components/Article";
import {
  getAll,
  getByCanonical,
} from "../../modules/article/services/article.service";
import Head from "next/head";

export default function ArticlePage({ article }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;title&amp;gt;{article.title}&amp;lt;/title&amp;gt;
      &amp;lt;/Head&amp;gt;
      &amp;lt;main&amp;gt;
        &amp;lt;Article {...article}&amp;gt;&amp;lt;/Article&amp;gt;
      &amp;lt;/main&amp;gt;

    &amp;lt;/div&amp;gt;
  );
}

export async function getStaticPaths() {
  const articles = await getAll();

  const paths = articles.map((article) =&amp;gt; ({
    params: { id: article.canonical },
  }));

  return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
  const article = await getByCanonical(params.id);

  return { props: { article } };
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Add styles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You can create any file .module.scss next to your component and your css classes will be mapped to an object you can import like
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// foo.module.scss
.container {
  display: block;
}

// foo.js
import styles from './foo.module.scss'

function Foo() {
  return &amp;lt;div className={styles.container}&amp;gt;My Foo styled component&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;For strapi deployment just go to your strapi project in a terminal and commit and push everything:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add --all
git commit -m "my changes"
git push heroku master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For the frontend:
frontend path and push your changes.&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;package.json&lt;/code&gt; change the build command for &lt;code&gt;next build &amp;amp;&amp;amp; next export&lt;/code&gt;. This will generate a static build into &lt;code&gt;/out&lt;/code&gt; folder. That's all we need to serve our site. To test it locally just run &lt;code&gt;npx serve out/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Then &lt;a href="https://docs.github.com/en/github/getting-started-with-github/create-a-repo"&gt;create a github repo&lt;/a&gt; and &lt;a href="https://docs.github.com/en/github/using-git/adding-a-remote"&gt;add your remote&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Then go to vercel.com and import your github project&lt;/li&gt;
&lt;li&gt;Remember to add your STRAPI_HOST env variable in vercel to point to your heroku database&lt;/li&gt;
&lt;li&gt;Follow the wizard and your app will be deployed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Updates
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a webhook in vercel that will provide you an url&lt;/li&gt;
&lt;li&gt;Log into your production strapi environment and in settings &amp;gt; webhooks create a new webhook with the provided url.&lt;/li&gt;
&lt;li&gt;Now every time you update your production database your blog will be rebuilt and redeployed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://nextjs-csr-blog.vercel.app/"&gt;Demo&lt;/a&gt; and &lt;a href="https://github.com/dieguezz/nextjs-static-blog"&gt;Code&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
