Deployment Guide: Deploying to Render (With Detailed Comments)

← Back to Main Tutorial

Understanding the Problem

Up to this point, we've been developing and testing our Express authentication application locally on our own computer. While this is perfect for development, eventually we need to make our application available to everyone on the internet. This process of making an application available online is called deployment.

Deployment involves several key challenges:

In this phase, we'll deploy our application to Render, a cloud platform that offers free hosting for web services and databases. Render makes it relatively easy to deploy Express applications, but there are several important steps and considerations we need to address.

Why Deployment Matters: Deployment is the bridge between development and actual users. A well-designed application is useless if users can't access it. The deployment process transforms your local application into a publicly available service.

Development vs. Production Environments: These environments have fundamental differences:

  • Development: Optimized for debugging, rapid changes, and developer convenience
  • Production: Optimized for performance, security, and stability

Why Render? There are many hosting platforms available (Heroku, AWS, DigitalOcean, etc.), but Render offers several advantages for this project:

  • Free tier for both web services and PostgreSQL databases
  • Relatively simple configuration process
  • Built-in support for Node.js applications
  • GitHub integration for automatic deployments
  • Environment variable management

SQLite to PostgreSQL: This transition is necessary because:

  • SQLite is a file-based database designed for local development
  • PostgreSQL is a robust, server-based database suitable for production
  • Cloud platforms like Render expect server-based databases

Planning the Solution

  1. Set up a package.json file at the project root
  2. Create a Render.com account
  3. Create a PostgreSQL database instance on Render
  4. Create a new web service on Render
  5. Configure build and start commands
  6. Set up environment variables
  7. Deploy the application
  8. Test the deployed application
  9. Understand ongoing maintenance requirements

Deployment Strategy: Our strategy follows a common pattern for deploying Node.js applications:

  1. Prepare the codebase with proper configuration and scripts
  2. Set up the database before the application (dependency order)
  3. Configure the application environment with necessary variables
  4. Deploy and test to verify functionality
  5. Plan for maintenance to ensure long-term reliability

Deployment Architecture: We'll be creating a simple two-tier architecture:

                ┌───────────────────────┐           ┌───────────────────────┐
                │                       │           │                       │
                │   Web Service         │           │   PostgreSQL Database │
                │   (Express App)       │◄─────────►│   (User data)         │
                │                       │           │                       │
                └───────────────────────┘           └───────────────────────┘
                        ▲
                        │
                        │ HTTP Requests
                        │
                        ▼
                ┌───────────────────────┐
                │                       │
                │   Clients             │
                │   (Web Browsers)      │
                │                       │
                └───────────────────────┘
                

This architecture separates the application logic (web service) from data storage (database), allowing each component to be managed, scaled, and secured independently.

Implementing the Solution

1. Set Up a package.json File at the Project Root

When deploying to Render, we need a package.json file at the root of our project (outside of both the backend and frontend folders). This file will define scripts that Render will use to install dependencies and start our application.

// Navigate to the project root (outside of backend and frontend)
cd ..
npm init -y

Now, let's modify the package.json to include the necessary scripts:

// package.json (in the project root)
{
  "name": "your-project-name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "install": "npm --prefix backend install backend",      // Install backend dependencies
    "dev:backend": "npm install --prefix backend start",    // Start backend in dev mode
    "sequelize": "npm run --prefix backend sequelize",      // Run sequelize commands 
    "sequelize-cli": "npm run --prefix backend sequelize-cli", // Run sequelize-cli
    "start": "npm start --prefix backend",                  // Start the backend
    "build": "npm run --prefix backend build"               // Build the backend
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

These scripts tell Render:

Commit these changes to your main branch:

git add .
git commit -m "Add root package.json for deployment"
git checkout main
git merge dev
git push origin main

Why a Root package.json? Render looks for a package.json file in the root of your repository to determine:

  • How to install dependencies
  • How to build the application
  • How to start the application

The --prefix Flag: This tells npm to run commands in a specific directory. For example, npm --prefix backend install runs npm install in the backend directory.

Script Chaining: Notice that some scripts call other scripts. For example, npm run --prefix backend sequelize runs the "sequelize" script defined in the backend's package.json.

Git Branch Management: For deployment, we're merging our development branch (dev) into the main branch. This is a common workflow:

  1. Develop features in feature branches
  2. Merge feature branches into dev for integration testing
  3. Merge dev into main for deployment

2. Set Up a Render.com Account

If you don't already have a Render.com account, you'll need to create one:

  1. Go to Render.com and click "Get Started"
  2. Sign up with GitHub (recommended) to easily connect your repositories
  3. Follow the instructions to complete your registration and verify your account

GitHub Integration: Signing up with GitHub offers several advantages:

  • Easy repository connection without manual configuration
  • Automatic deployments when you push to your repository
  • No need to manually upload code or set up SSH keys

Account Verification: Render typically requires email verification and may also require a credit card for verification (even for free tier usage). This helps prevent abuse of their services.

3. Create a PostgreSQL Database Instance

Now, let's create a PostgreSQL database that our application will use in production:

  1. From your Render dashboard, click "New +" and select "PostgreSQL"
  2. Give your database a name (e.g., "app-academy-projects")
  3. Choose the region closest to you
  4. Leave the other fields with their default values
  5. Click "Create Database"

After the database is created (which may take a few minutes), Render will display important information about your database, including:

Take note of the "Internal Database URL" - we'll need this later.

PostgreSQL vs. SQLite: In development, we used SQLite because it's simple (just a file) and requires no setup. For production, we're switching to PostgreSQL because:

  • PostgreSQL handles concurrent connections better
  • It's more robust for production workloads
  • It offers better performance and reliability
  • It supports more advanced features (like JSON fields, full-text search, etc.)

Database Connection URL Format: The Internal Database URL will look like this:

postgres://username:password@hostname:port/database_name

Internal vs. External URL: Render provides two URLs:

  • Internal URL: For connections from other Render services (faster, more secure)
  • External URL: For connections from outside Render (useful for database management tools)

We'll use the Internal URL since our web service will also be on Render.

4. Create a New Web Service

Next, let's create a web service to host our Express application:

  1. From your Render dashboard, click "New +" and select "Web Service"
  2. Connect your GitHub repository
  3. If you don't see your repository, click "Configure Account" for GitHub in the sidebar to connect your GitHub account
  4. Find your project repository and click "Connect"

Web Service Types on Render: Render offers different service types:

  • Web Services: For APIs, websites, and applications (what we're using)
  • Static Sites: For HTML/CSS/JS websites with no server-side code
  • Background Workers: For long-running tasks and job processing
  • Cron Jobs: For scheduled tasks

We're using a Web Service because our Express application is a dynamic API that handles HTTP requests and interacts with a database.

Repository Connection: When you connect a GitHub repository, Render sets up a webhook to be notified of new commits. This enables the automatic deployment feature.

5. Configure the Web Service

Now, configure the web service with the following settings:

npm install && npm run build && npm run sequelize --prefix backend db:seed:undo:all && npm run sequelize --prefix backend db:migrate:undo:all && npm run sequelize --prefix backend db:migrate && npm run sequelize --prefix backend db:seed:all

This build command:

  1. Installs dependencies
  2. Runs the build script
  3. Undoes all seed data and migrations
  4. Runs all migrations to set up the database schema
  5. Seeds the database with initial data

Note: This build command is designed for development and testing. For a final production deployment, you might want to remove the "undo" steps to preserve your data:

npm install && npm run build && npm run sequelize --prefix backend db:migrate && npm run sequelize --prefix backend db:seed:all

Build Command Breakdown: Let's analyze the complete build command:

  1. npm install: Installs root dependencies and (via the install script) backend dependencies
  2. npm run build: Runs the build script, which calls the backend build script for production compilation
  3. npm run sequelize --prefix backend db:seed:undo:all: Reverses all seed data
  4. npm run sequelize --prefix backend db:migrate:undo:all: Reverses all migrations
  5. npm run sequelize --prefix backend db:migrate: Runs all migrations to set up tables
  6. npm run sequelize --prefix backend db:seed:all: Seeds the database with initial data

Why Undo Migrations and Seeds? This approach ensures a clean slate for every deployment, which is useful during development and testing. It effectively resets the database structure and initial data.

Production Considerations: For a real production application with user data, you would:

  • Remove the undo steps to preserve existing data
  • Use migration files that add/modify tables without data loss
  • Consider using a separate seed process for initial setup only

Region Selection: Placing your web service in the same region as your database reduces latency for database operations. This improves performance, especially for database-intensive applications.

6. Set Up Environment Variables

Scroll down to the "Environment Variables" section and add the following variables:

Make sure "Auto-Deploy" is set to "Yes" in the Advanced section. This will automatically redeploy your application when you push to the main branch.

Environment Variables in Production: Environment variables serve several important purposes:

  • Keep sensitive information out of your codebase
  • Configure environment-specific behavior
  • Simplify deployment to different environments

Key Variables Explained:

  • JWT_SECRET: Used to sign and verify JWTs; must be kept secret and should be different between environments
  • JWT_EXPIRES_IN: Token lifetime in seconds (604800 = 7 days)
  • NODE_ENV=production: Tells Express to run in production mode (optimized for performance, less verbose errors)
  • SCHEMA: Used by Sequelize to namespace your tables in the PostgreSQL database
  • DATABASE_URL: Connection string for the PostgreSQL database

Schema in PostgreSQL: In PostgreSQL, a schema is a namespace that contains database objects like tables. Using a custom schema helps:

  • Organize tables logically
  • Avoid name conflicts with other applications using the same database
  • Simplify access control and permissions

Auto-Deploy: This feature automatically triggers a new deployment when changes are pushed to the specified branch (main). It streamlines the development workflow by eliminating manual deployment steps.

7. Deploy the Application

Click "Create Web Service" to start the deployment process. This will take some time (usually 10-15 minutes) as Render:

  1. Builds your application according to the build command
  2. Sets up the environment variables
  3. Starts your application using the start command

You can monitor the progress in the logs. Once the deployment is complete, Render will provide a URL where your application is accessible (e.g., https://your-app-name.onrender.com).

Deployment Process on Render: Behind the scenes, Render:

  1. Provisions a virtual instance for your application
  2. Clones your GitHub repository
  3. Sets up the environment (Node.js, environment variables)
  4. Runs the build command
  5. Starts your application
  6. Sets up a domain and SSL certificate

Deployment Logs: The logs are crucial for troubleshooting. They show:

  • The output of each build step
  • Any errors that occur during deployment
  • The standard output (console.log) from your application

SSL Certificates: Render automatically provisions free SSL certificates for all services, ensuring that your application is accessible via HTTPS. This is important for:

  • Secure cookie transmission
  • Protected data exchange
  • Browser compatibility (many modern features require HTTPS)

8. Test the Deployed Application

Let's test our deployed API to make sure everything is working correctly:

  1. First, test the CSRF token route: https://your-app-name.onrender.com/api/csrf/restore
  2. You should see a JSON response with a CSRF token
  3. Use this token to test your login endpoint using a tool like Postman or a fetch request in a browser console

Example fetch request to test login:

fetch('https://your-app-name.onrender.com/api/session', {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",               // Set content type
    "XSRF-TOKEN": "your-csrf-token-here"              // Include CSRF token from cookie
  },
  body: JSON.stringify({ credential: 'Demo-lition', password: 'password' })  // Login credentials
}).then(res => res.json()).then(data => console.log(data));

Test other endpoints similarly to ensure they're all working correctly.

Systematic Testing Approach: When testing a deployed application, work methodically:

  1. Test Prerequisites: First test the CSRF token endpoint since other endpoints depend on it
  2. Test Core Functionality: Then test authentication endpoints (login, signup, session)
  3. Test Edge Cases: Try invalid credentials, malformed requests, etc.

Cross-Origin Considerations: When testing from a browser console on a different domain, you may encounter CORS issues. Options to handle this:

  • Test from the deployed domain's console (if you have a frontend deployed)
  • Use Postman or another API testing tool
  • Configure CORS in your backend to allow specific origins

Cookie Handling: The fetch example assumes you're testing in a browser that can handle cookies. If using Postman or another tool, you may need to manually handle cookies or use the withCredentials option.

9. Understand Ongoing Maintenance

With Render's free tier, there's an important limitation to be aware of: your PostgreSQL database instance will be deleted after 90 days. To keep your application running, you'll need to create a new database instance before that happens.

Set a calendar reminder for 85 days from now (to give yourself some buffer) with these steps:

  1. Create a new PostgreSQL database instance on Render
  2. Update the DATABASE_URL environment variable in your web service with the new URL
  3. Manually trigger a new deployment

This will ensure your application continues to run smoothly without data loss.

Free Tier Limitations: Render's free tier has several limitations to be aware of:

  • PostgreSQL databases expire after 90 days (the main limitation we need to address)
  • Limited computational resources (slower performance)
  • Services spin down after periods of inactivity (causing slow initial responses)
  • Limited bandwidth and storage

Database Migration Process: When creating a new database after 90 days:

  1. Create the new database instance
  2. Update the DATABASE_URL environment variable
  3. Deploy your application, which will run migrations and seeds

Data Persistence Strategy: For applications with important user data, consider:

  • Regular database backups (can be manual on free tier)
  • Upgrading to a paid tier for persistent databases
  • Implementing data export/import functionality

Monitoring: Regularly check your Render dashboard for:

  • Service health and uptime
  • Error logs and issues
  • Resource usage (approaching limits)
  • Upcoming database expiration

Review the Solution

We've successfully deployed our Express authentication application to Render! Let's review what we've accomplished:

Key Accomplishments Explained:

  • Project Structure: Created a root package.json that directs Render to our backend application
  • Database Setup: Provisioned a PostgreSQL database and configured our application to use it
  • Environment Configuration: Set up production-specific settings via environment variables
  • Build Process: Created a build process that prepares the application for production
  • Deployment: Successfully deployed the application to a public URL
  • Maintenance Plan: Understood and planned for the 90-day database limitation

Skills Acquired: Through this deployment process, you've gained valuable skills in:

  • Cloud platform configuration
  • Environment management
  • Database setup and connection
  • Production deployment workflows
  • Build and start script configuration
  • Application testing in production

Deployment Architecture

Our deployment architecture consists of two main components:

  1. PostgreSQL Database: Stores our user data and other application information
  2. Web Service: Runs our Express application, handling HTTP requests and interacting with the database

This is a simple but effective architecture for many web applications. As your application grows, you might consider more complex architectures with separate services for different parts of your application, content delivery networks (CDNs) for static assets, and caching layers for improved performance.

Scaling This Architecture: As your application grows, this architecture can evolve:

                  BASIC ARCHITECTURE                SCALED ARCHITECTURE
                  ------------------                -------------------
                                               
┌──────────────┐   ┌──────────────┐     ┌────────┐    ┌──────────────┐   ┌──────────────┐
│              │   │              │     │        │    │              │   │              │
│ Web Service  │◄─►│  PostgreSQL  │     │  CDN   │◄─►│ Web Services │◄─►│  PostgreSQL  │
│              │   │              │     │        │    │ (Load Balanced)  │  (Replicated) │
└──────────────┘   └──────────────┘     └────────┘    └──────────────┘   └──────────────┘
        ▲                                    ▲                 ▲
        ▲                                    ▲                 ▲                ▲
        │                                    │                 │                │
        │                                    │                 │                │
        ▼                                    ▼                 ▼                ▼
┌──────────────┐                      ┌────────────┐    ┌────────────┐  ┌────────────┐
│              │                      │            │    │            │  │            │
│   Clients    │                      │  Clients   │    │ Cache Layer│  │  Message   │
│              │                      │            │    │            │  │   Queue    │
└──────────────┘                      └────────────┘    └────────────┘  └────────────┘
                

Scaling Options:

  • Horizontal Scaling: Add more web service instances behind a load balancer
  • Database Replication: Create read replicas for improved query performance
  • CDN Integration: Serve static assets through a Content Delivery Network
  • Caching: Add Redis or Memcached to cache frequently accessed data
  • Message Queues: Handle asynchronous tasks or background processing

Microservices Evolution: As complexity grows, you might split your monolithic application into smaller, focused services:

  • Authentication Service
  • User Profile Service
  • Content Service
  • Notification Service
  • etc.

Development vs. Production Environments

It's important to understand the key differences between our development and production environments:

Aspect Development (Local) Production (Render)
Database SQLite (file-based) PostgreSQL (server-based)
Environment development production
Hosting Local machine Render cloud platform
Error Visibility Detailed (includes stack traces) Limited (no stack traces)
Security Settings More relaxed (e.g., CORS enabled) Stricter (e.g., secure cookies required)

Environment Differences in Detail:

Database:

  • Development: SQLite stores data in a single file, ideal for development but limited in concurrent access
  • Production: PostgreSQL offers better performance, concurrency, and reliability

Error Handling:

  • Development: Verbose errors with stack traces help debugging
  • Production: Limited error details prevent exposing sensitive information to users

Performance Optimization:

  • Development: Optimized for quick feedback and debugging
  • Production: Optimized for response speed and resource efficiency

Security Measures:

  • Development: Often lacks HTTPS, has relaxed CORS
  • Production: HTTPS required, strict CORS, secure cookies, rate limiting

Code Execution:

  • Development: Uses tools like nodemon to auto-restart on changes
  • Production: Uses process managers to handle crashes and ensure uptime

Real-world Deployment Considerations

In real-world applications, deployment often involves additional considerations:

While our deployment is relatively simple, these principles are still important to consider, especially as your application grows.

CI/CD Pipeline Example: A more advanced deployment setup might include:

┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│             │   │             │   │             │   │             │   │             │
│  Developer  │──►│ GitHub Repo │──►│ CI Service  │──►│  Staging    │──►│ Production  │
│  Commit     │   │  (PR/Merge) │   │ (Tests/Lints)   │ Environment │   │ Deployment  │
│             │   │             │   │             │   │             │   │             │
└─────────────┘   └─────────────┘   └─────────────┘   └─────────────┘   └─────────────┘
                

Monitoring and Alerting: Production applications need monitoring for:

  • Performance: Response times, throughput, resource usage
  • Errors: Exception tracking, error rates, API failures
  • Business Metrics: User signups, active users, conversion rates
  • Infrastructure: Server health, database performance, network issues

Tools for Production Management:

  • Logging: ELK Stack (Elasticsearch, Logstash, Kibana), Papertrail
  • Monitoring: New Relic, Datadog, Prometheus + Grafana
  • Error Tracking: Sentry, Rollbar
  • Performance: Lighthouse, WebPageTest
  • Security: Snyk, OWASP ZAP, npm audit

Common Issues and Solutions

Troubleshooting Techniques:

Build Failures:

  • Examine the specific error message in the build logs
  • Try to reproduce the issue locally by running the build command
  • Check for differences between your local and production environments
  • Verify package versions and compatibility

Database Connection Issues:

// Common error pattern
SequelizeConnectionError: connect ECONNREFUSED 127.0.0.1:5432
                

Solutions:

  • Verify DATABASE_URL is correctly formatted
  • Check that the database service is running
  • Confirm your application is using the environment variable correctly
  • Test database connection separately using a tool like psql

Common CORS Error:

Access to fetch at 'https://your-api.onrender.com/api/session' from origin 'https://your-frontend.com' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
                

Solutions:

  • Configure your backend to allow specific origins:
  • // In your Express app setup
    if (process.env.NODE_ENV === 'production') {
      const allowedOrigins = ['https://your-frontend.com'];
      app.use(cors({
        origin: function(origin, callback) {
          if (!origin || allowedOrigins.indexOf(origin) !== -1) {
            callback(null, true);
          } else {
            callback(new Error('Not allowed by CORS'));
          }
        },
        credentials: true
      }));
    }
                        

Debugging in Production

When issues occur in production, you have several tools at your disposal:

  1. Render Logs: Check the logs in your Render dashboard for error messages and application output.
  2. Manual Testing: Use tools like Postman or browser fetch requests to test endpoints directly.
  3. Database Inspection: Connect to your PostgreSQL database to verify data and schema.
  4. Local Replication: Try to replicate the issue locally by setting up similar environment variables.

Remember, debugging in production requires a careful, methodical approach to avoid introducing new issues or causing downtime.

Production Debugging Best Practices:

1. Accessing Render Logs:

  • Navigate to your Web Service in the Render dashboard
  • Click on the "Logs" tab
  • Use the search function to find specific errors
  • Check both "Build Logs" and "Runtime Logs"

2. Enhanced Logging: Consider adding more detailed logging in your application:

// Example of enhanced logging
const logRequest = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - User: ${req.user ? req.user.id : 'anonymous'}`);
  next();
};

app.use(logRequest);
                

3. Database Inspection: You can connect to your Render PostgreSQL database using:

  • The psql command line tool
  • A GUI tool like pgAdmin or TablePlus
  • Use the External Database URL from your Render dashboard

Example psql connection:

psql postgres://username:password@hostname:port/database
                

4. Safe Testing Techniques:

  • Use read-only operations when possible
  • Test on staging environments before production
  • Make backups before making changes
  • Use transactions for database operations

Conclusion

Congratulations! You've successfully built and deployed a complete Express authentication application. This application provides a solid foundation for any web project that requires user authentication. You can now build frontend applications that use this API for authentication, or extend the API with additional features specific to your application.

Throughout this tutorial, you've learned how to:

These skills are fundamental to full-stack web development and will serve you well in building a wide variety of web applications.

Key Skills Summary:

Backend Development:

  • RESTful API design
  • Middleware implementation
  • Database interaction with ORM
  • Authentication and security
  • Error handling

DevOps and Deployment:

  • Environment configuration
  • Build and deployment processes
  • Database setup and migration
  • Production troubleshooting
  • Cloud platform management

Security Practices:

  • Password hashing
  • JWT-based authentication
  • CSRF protection
  • Input validation
  • Environment-specific security settings

These skills represent a solid foundation for a backend developer role and provide transferable knowledge for various web development projects.

Next Steps

Now that you have a working authentication API, here are some ways you might extend or build upon it:

Whatever path you choose, the authentication system you've built provides a solid foundation for your application's security needs.

Project Extension Ideas:

1. Enhanced Authentication Features:

  • Multi-factor Authentication: Add a second authentication factor like SMS or authenticator app
  • OAuth Integration: Allow login with Google, Facebook, GitHub, etc.
  • Account Recovery: Implement secure password reset and account recovery workflows
  • Session Management: Add the ability to view and manage active sessions

2. Frontend Implementation:

  • React: Build a SPA with React Router and authentication context
  • Mobile: Create a React Native mobile app that uses your API
  • Admin Dashboard: Build an admin interface for user management

3. API Extensions:

  • Content Management: Add endpoints for creating and managing content
  • File Upload: Implement secure file uploading with AWS S3 or similar
  • Analytics: Add endpoints for reporting and analytics
  • Real-time Features: Integrate WebSockets for chat or notifications

4. Production Enhancements:

  • Rate Limiting: Protect against brute force attacks and API abuse
  • Caching: Implement Redis or another caching solution
  • Logging: Set up comprehensive logging with a service like LogDNA
  • Monitoring: Add application performance monitoring

These extensions can transform your authentication API into a full-featured application platform tailored to your specific needs.