Deploying a Next.js 15 Blog to DigitalOcean App Platform: A Complete Guide
Building a personal blog is one thing, but deploying it to production where the world can see it? That's where the real adventure begins. Today, I'll walk you through exactly how I deployed my Next.js 15 blog to DigitalOcean App Platform, including all the gotchas and solutions I encountered along the way.
The Tech Stack
Before diving into deployment, here's what we're working with:
- Frontend: Next.js 15 with App Router
- Database: Supabase (PostgreSQL)
- Authentication: WorkOS AuthKit
- Styling: Tailwind CSS
- Deployment: DigitalOcean App Platform
Why DigitalOcean App Platform?
I chose DigitalOcean App Platform for several reasons:
- Simplicity: Git-based deployments with automatic builds
- Cost-effective: Starting at $5/month for small apps
- Integrated: Built-in monitoring, logging, and scaling
- Developer-friendly: Great documentation and support
The Deployment Process
Step 1: Preparing the Repository
First, I needed to ensure my code was production-ready. This meant adding some configuration files:
Next.js Configuration (next.config.ts):
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**',
},
],
},
eslint: {
ignoreDuringBuilds: true,
},
};
export default nextConfig;The output: 'standalone' is crucial for App Platform deployments as it creates a self-contained application.
Step 2: Handling Dependency Conflicts
Here's where I hit my first major roadblock. Next.js 15 and WorkOS AuthKit have a peer dependency conflict:
ERESOLVE could not resolve
While resolving: @workos-inc/[email protected]WorkOS AuthKit currently supports Next.js 13.5.4 || ^14.0.3, but I was using Next.js 15.4.5.
Solution: I created an .npmrc file to handle this:
legacy-peer-deps=true
strict-peer-deps=falseThis tells npm to use the older, more permissive dependency resolution algorithm.
Step 3: Creating the App Platform Configuration
I created a .do/app.yaml file to define the deployment configuration:
name: next-blog
services:
- name: web
source_dir: /
github:
repo: your-username/next-blog
branch: main
run_command: npm start
build_command: npm install --legacy-peer-deps && npm run build
environment_slug: node-js
instance_count: 1
instance_size_slug: basic-xxs
routes:
- path: /
env:
- key: NODE_ENV
value: production
# ... other environment variablesNotice the --legacy-peer-deps flag in the build command - this was essential for resolving the dependency conflicts.
Step 4: Fixing the Marked Library Issue
During the build process, I encountered another TypeScript error:
Type error: Object literal may only specify known properties,
and 'highlight' does not exist in type 'MarkedOptions'.The marked library had updated its API, removing the highlight option. I had to refactor my markdown renderer:
Before:
marked.setOptions({
highlight: function(code, lang) {
if (lang && Prism.languages[lang]) {
return Prism.highlight(code, Prism.languages[lang], lang)
}
return code
},
});After:
const renderer = new marked.Renderer()
renderer.code = function(code, language) {
const validLanguage = language && Prism.languages[language] ? language : 'text'
const highlightedCode = Prism.languages[validLanguage]
? Prism.highlight(code, Prism.languages[validLanguage], validLanguage)
: code
return `<pre><code class="language-${validLanguage}">${highlightedCode}</code></pre>`
}
marked.setOptions({
renderer: renderer,
})Step 5: Setting Up Environment Variables
One of the biggest differences between local development and production is environment variable management. In DigitalOcean App Platform, you set these through the dashboard:
- Go to Apps → Select your app
- Navigate to Settings → Environment Variables
- Add all required variables:
NODE_ENV=production
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
WORKOS_API_KEY=your_workos_api_key
WORKOS_CLIENT_ID=your_workos_client_id
WORKOS_COOKIE_PASSWORD=32_character_random_string
WORKOS_REDIRECT_URI=https://your-app.ondigitalocean.app/api/auth/callback
NEXT_PUBLIC_APP_URL=https://your-app.ondigitalocean.app
NEXT_PUBLIC_BLOG_TITLE=Your Blog Title
NEXT_PUBLIC_BLOG_DESCRIPTION=Your blog descriptionPro tip: Generate a strong 32-character string for the cookie password. Security matters!
Step 6: Updating External Services
Don't forget to update your external services with the new production URLs:
Supabase:
- Add your app domain to allowed origins
- Update any hardcoded URLs in your RLS policies
WorkOS:
- Add your production domain as an allowed redirect URI
- Update the callback URL to match your deployment
Step 7: Database Setup
I had to run several SQL scripts in Supabase to set up the production database:
- Initial schema (
supabase/schema.sql) - Updated policies (
supabase/updated-policies.sql) - Hero content fields (
supabase/add-hero-content.sql)
This ensures the production database has all the necessary tables and security policies.
The Deployment Experience
Once everything was configured, the actual deployment was surprisingly smooth:
- Push to GitHub: All changes committed and pushed
- Create App: Connected GitHub repository to DigitalOcean
- Configure Build: Set build commands and environment variables
- Deploy: Watched the magic happen in real-time
The build took about 5-7 minutes, and then my blog was live on the internet!
Post-Deployment Tasks
After deployment, there were a few housekeeping tasks:
- Admin Setup: Visited
/admin-loginto create my admin account - Profile Configuration: Set up my avatar and hero content
- Test Post: Created a test blog post to verify everything worked
- DNS Setup: (Optional) Configure custom domain
Performance and Monitoring
DigitalOcean App Platform provides excellent monitoring out of the box:
- Real-time logs: Perfect for debugging
- Performance metrics: CPU, memory, and request metrics
- Automatic scaling: Handles traffic spikes gracefully
- SSL certificates: Automatic HTTPS with Let's Encrypt
Cost Breakdown
Here's what it costs to run this setup:
- DigitalOcean App Platform: $5/month (Basic plan)
- Supabase: Free tier (sufficient for personal blogs)
- WorkOS: Free tier (up to 7,500 MAUs)
- Domain: ~$10/year (optional)
Total: About $5/month for a fully-featured, production-ready blog.
Lessons Learned
- Dependency Management: Always check peer dependencies when using bleeding-edge versions
- Environment Variables: Production environment setup is crucial - test early
- Documentation: Keep detailed deployment notes for future reference
- Monitoring: Set up alerts from day one
- Backups: Supabase handles this, but verify your backup strategy
Common Gotchas and Solutions
Build Fails with ERESOLVE:
- Use
--legacy-peer-depsin build command - Add
.npmrcfile with legacy peer deps enabled
Authentication Issues:
- Double-check WorkOS redirect URIs
- Ensure HTTPS in production
- Verify cookie settings
Database Connection Issues:
- Check Supabase allowed origins
- Verify environment variables
- Review RLS policies
Final Thoughts
Deploying a modern Next.js application doesn't have to be complicated. DigitalOcean App Platform provides an excellent balance of simplicity and power, making it perfect for personal projects and small applications.
The key is preparation: understanding your dependencies, configuring your environment properly, and having a clear deployment checklist. Once you've done it once, subsequent deployments become almost trivial.
Would I recommend this stack? Absolutely. The developer experience is fantastic, the costs are reasonable, and the performance is excellent. Plus, having your own corner of the internet to share your thoughts and projects? Priceless.
Resources
- DigitalOcean App Platform Documentation
- Next.js Deployment Documentation
- Supabase Documentation
- WorkOS Documentation
This blog post is written on the very platform I just deployed. Meta? Perhaps. Satisfying? Definitely.
What's your experience with Next.js deployments? Have you tried DigitalOcean App Platform? Let me know in the comments below!