Skip to main content
This guide covers deploying Macondo Link Manager to production environments. The application is currently deployed using Vercel (frontend) and Railway (backend + database).

Production URLs

The current production deployment:

Architecture Overview

Macondo Link Manager uses a split deployment strategy:
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   Vercel    │─────▶│   Railway   │─────▶│  PostgreSQL │
│  (Frontend) │ HTTPS │  (Backend)  │      │  (Railway)  │
└─────────────┘      └─────────────┘      └─────────────┘
  • Frontend (Next.js): Deployed on Vercel with custom domain
  • Backend (Fastify): Deployed on Railway with custom domain
  • Database (PostgreSQL): Managed database on Railway

Deployment Options

Vercel + Railway

Current production setup - Recommended for most use cases

Docker Deployment

Self-hosted option using Docker containers
This is the current production setup used by Macondo.

Prerequisites

  • Vercel account
  • Railway account
  • Custom domain (optional but recommended)
  • Google OAuth credentials configured for production URLs

Deploy Backend to Railway

1
Create Railway project
2
  • Go to Railway
  • Click New Project
  • Select Deploy from GitHub repo
  • Connect your repository
  • 3
    Add PostgreSQL database
    4
  • In your Railway project, click New
  • Select DatabasePostgreSQL
  • Railway will automatically provision the database
  • Copy the DATABASE_URL connection string
  • 5
    Configure backend service
    6
  • Click on your API service
  • Go to SettingsRoot Directory
  • Set to api
  • Under Build, set build command:
    npm install && npx prisma generate && npm run build
    
  • Set start command:
    sh -c "./scripts/download-geolite.sh || true; node dist/server.js"
    
  • 7
    Set environment variables
    8
    Add the following environment variables in Railway:
    9
    DATABASE_URL=${{Postgres.DATABASE_URL}}
    BASE_URL=https://li.mcd.ppg.br
    GOOGLE_CLIENT_ID=your_production_client_id
    GOOGLE_CLIENT_SECRET=your_production_client_secret
    JWT_SECRET=your_production_jwt_secret
    FRONTEND_URL=https://app.mcd.ppg.br
    NODE_ENV=production
    NODE_VERSION=20
    PORT=3333
    
    10
    DATABASE_URL will be automatically populated by Railway when you reference the PostgreSQL service.
    11
    Run database migrations
    12
    After deployment, run migrations using Railway CLI:
    13
    # Install Railway CLI
    npm i -g @railway/cli
    
    # Login
    railway login
    
    # Link to your project
    railway link
    
    # Run migrations
    railway run npx prisma migrate deploy
    
    14
    Alternatively, add a migration step to your Dockerfile:
    15
    CMD ["sh", "-c", "npx prisma migrate deploy && node dist/server.js"]
    
    16
    Configure custom domain
    17
  • In Railway, go to your API service
  • Click SettingsNetworking
  • Add custom domain: li.mcd.ppg.br
  • Update your DNS records as instructed
  • Wait for SSL certificate provisioning
  • Deploy Frontend to Vercel

    1
    Create Vercel project
    2
  • Go to Vercel
  • Click New Project
  • Import your Git repository
  • Configure the project:
    • Framework: Next.js
    • Root Directory: web
    • Build Command: npm run build
    • Output Directory: .next
  • 3
    Set environment variables
    4
    Add environment variables in Vercel:
    5
    NEXT_PUBLIC_API_URL=https://li.mcd.ppg.br
    
    6
    Deploy
    7
    Click Deploy. Vercel will automatically build and deploy your frontend.
    8
    Configure custom domain
    9
  • Go to SettingsDomains
  • Add custom domain: app.mcd.ppg.br
  • Update DNS records as instructed
  • Vercel automatically provisions SSL certificates
  • Update OAuth Redirect URIs

    Don’t forget to update Google OAuth settings with production URLs.
    In Google Cloud Console:
    1. Go to APIs & ServicesCredentials
    2. Edit your OAuth 2.0 Client ID
    3. Add authorized redirect URI:
      https://li.mcd.ppg.br/auth/google/callback
      
    4. Add authorized JavaScript origins:
      https://app.mcd.ppg.br
      

    Docker Deployment

    For self-hosted deployments using Docker.

    Production Dockerfile

    The API includes a multi-stage production Dockerfile:
    # Stage 1: Build
    FROM node:20-alpine AS build
    WORKDIR /app
    
    COPY package*.json ./
    RUN npm install
    
    COPY . .
    COPY data ./data
    
    RUN apk add --no-cache curl tar
    RUN npx prisma generate
    RUN npm run build
    
    # Stage 2: Runtime
    FROM node:20-alpine
    WORKDIR /app
    
    RUN apk add --no-cache curl tar
    
    COPY --from=build /app/package*.json ./
    COPY --from=build /app/node_modules ./node_modules
    COPY --from=build /app/dist ./dist
    COPY --from=build /app/prisma ./prisma
    COPY --from=build /app/data ./data
    COPY --from=build /app/scripts ./scripts
    
    ENV NODE_ENV=production
    EXPOSE 3333
    
    CMD ["sh", "-c", "./scripts/download-geolite.sh || true; node dist/server.js"]
    

    Deploy with Docker Compose

    1
    Create production docker-compose
    2
    Create docker-compose.prod.yml:
    3
    services:
      postgres:
        image: postgres:15-alpine
        container_name: macondo_links_db_prod
        restart: always
        environment:
          POSTGRES_DB: ${POSTGRES_DB}
          POSTGRES_USER: ${POSTGRES_USER}
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
        volumes:
          - postgres_data:/var/lib/postgresql/data
        networks:
          - macondo-network
    
      api:
        build:
          context: ./api
          dockerfile: Dockerfile
        container_name: macondo_links_api_prod
        restart: always
        ports:
          - "3333:3333"
        environment:
          DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
          BASE_URL: ${BASE_URL}
          GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
          GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
          JWT_SECRET: ${JWT_SECRET}
          FRONTEND_URL: ${FRONTEND_URL}
          NODE_ENV: production
          PORT: 3333
        depends_on:
          - postgres
        networks:
          - macondo-network
    
      web:
        build:
          context: ./web
          dockerfile: Dockerfile
        container_name: macondo_links_web_prod
        restart: always
        ports:
          - "3000:3000"
        environment:
          NEXT_PUBLIC_API_URL: ${BASE_URL}
        depends_on:
          - api
        networks:
          - macondo-network
    
    volumes:
      postgres_data:
    
    networks:
      macondo-network:
        driver: bridge
    
    4
    Configure production environment
    5
    Create .env.production:
    6
    POSTGRES_DB=macondo_links_prod
    POSTGRES_USER=postgres
    POSTGRES_PASSWORD=strong_production_password
    
    BASE_URL=https://li.mcd.ppg.br
    FRONTEND_URL=https://app.mcd.ppg.br
    
    GOOGLE_CLIENT_ID=production_client_id
    GOOGLE_CLIENT_SECRET=production_client_secret
    JWT_SECRET=strong_random_jwt_secret
    
    NODE_ENV=production
    
    7
    Use strong passwords and secrets in production. Never commit .env.production to version control.
    8
    Build and deploy
    9
    # Build images
    docker-compose -f docker-compose.prod.yml build
    
    # Start services
    docker-compose -f docker-compose.prod.yml up -d
    
    # Run database migrations
    docker-compose -f docker-compose.prod.yml exec api npx prisma migrate deploy
    
    # Check status
    docker-compose -f docker-compose.prod.yml ps
    
    10
    Set up reverse proxy
    11
    Use Nginx or Caddy to handle SSL and route traffic:
    12
    # Nginx configuration
    server {
        listen 443 ssl http2;
        server_name li.mcd.ppg.br;
    
        ssl_certificate /path/to/cert.pem;
        ssl_certificate_key /path/to/key.pem;
    
        location / {
            proxy_pass http://localhost:3333;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    
    server {
        listen 443 ssl http2;
        server_name app.mcd.ppg.br;
    
        ssl_certificate /path/to/cert.pem;
        ssl_certificate_key /path/to/key.pem;
    
        location / {
            proxy_pass http://localhost:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    

    Database Migrations in Production

    Always backup your database before running migrations in production.

    Railway

    Run migrations using Railway CLI:
    railway run npx prisma migrate deploy
    

    Docker

    Run migrations in the API container:
    docker-compose exec api npx prisma migrate deploy
    

    Manual

    For direct database access:
    # Set production DATABASE_URL
    export DATABASE_URL="postgresql://..."
    
    # Deploy migrations
    cd api
    npx prisma migrate deploy
    

    Environment Variables

    Critical Production Variables

    VariableRequiredDescription
    DATABASE_URLPostgreSQL connection string
    GOOGLE_CLIENT_IDGoogle OAuth Client ID
    GOOGLE_CLIENT_SECRETGoogle OAuth Secret
    JWT_SECRETJWT signing secret (must be strong)
    BASE_URLPublic API URL (e.g., https://li.mcd.ppg.br)
    FRONTEND_URLPublic frontend URL (e.g., https://app.mcd.ppg.br)
    NODE_ENVMust be production
    PORTAPI port (default: 3333)
    MAXMIND_LICENSE_KEY⚠️Optional: for GeoIP data
    Generate a strong JWT_SECRET using a cryptographically secure random generator:
    openssl rand -base64 32
    

    Health Checks

    Monitor your deployment with health checks:
    curl https://li.mcd.ppg.br/health
    
    Expected response:
    {
      "status": "ok",
      "dbConnection": "healthy"
    }
    

    Set up monitoring

    Recommended monitoring tools:
    • Railway: Built-in metrics and logs
    • Vercel: Analytics and deployment logs
    • UptimeRobot: External uptime monitoring
    • Sentry: Error tracking (to be implemented)

    Domain Configuration

    DNS Records

    For custom domains, configure these DNS records: Frontend (Vercel):
    app.mcd.ppg.br  CNAME  cname.vercel-dns.com
    
    Backend (Railway):
    li.mcd.ppg.br   CNAME  provided-by-railway.up.railway.app
    

    SSL Certificates

    Both Vercel and Railway automatically provision and renew SSL certificates using Let’s Encrypt. For self-hosted deployments, use:
    • Certbot for Let’s Encrypt certificates
    • Caddy for automatic HTTPS

    Rollback Strategy

    Vercel

    Vercel keeps all previous deployments:
    1. Go to Deployments
    2. Find the stable version
    3. Click Promote to Production

    Railway

    Railway maintains deployment history:
    1. Go to Deployments
    2. Select a previous deployment
    3. Click Redeploy

    Docker

    Tag your images for easy rollback:
    # Tag current version
    docker tag macondo-api:latest macondo-api:v1.4.0
    
    # Rollback to specific version
    docker-compose down
    docker-compose up -d macondo-api:v1.3.0
    

    Database Backups

    Railway Automated Backups

    Railway Pro plan includes automated daily backups.

    Manual Backup

    Create manual backups:
    # Backup
    pg_dump $DATABASE_URL > backup_$(date +%Y%m%d_%H%M%S).sql
    
    # Restore
    psql $DATABASE_URL < backup_20260309_120000.sql
    

    Automated Backup Script

    #!/bin/bash
    BACKUP_DIR="/backups"
    DATABASE_URL="your_database_url"
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    
    mkdir -p $BACKUP_DIR
    pg_dump $DATABASE_URL | gzip > $BACKUP_DIR/backup_$TIMESTAMP.sql.gz
    
    # Keep only last 30 days
    find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
    
    Schedule with cron:
    0 2 * * * /path/to/backup.sh
    

    Performance Optimization

    Backend

    • Enable connection pooling in Prisma
    • Use Redis for caching (future enhancement)
    • Optimize database queries with indexes
    • Enable compression in Fastify

    Frontend

    • Enable Next.js static optimization
    • Use Vercel Edge Network for fast global delivery
    • Implement proper caching headers
    • Optimize images with Next.js Image component

    Database

    • Create indexes on frequently queried fields
    • Use connection pooling (PgBouncer)
    • Monitor slow queries
    • Regular VACUUM and ANALYZE

    Security Checklist

    • Strong JWT_SECRET (32+ characters, random)
    • HTTPS enabled on all domains
    • Google OAuth restricted to corporate domain
    • Environment variables not committed to Git
    • Database backups configured
    • Health check endpoint monitored
    • CORS configured correctly
    • Rate limiting enabled (future)
    • SQL injection protection (Prisma ORM)
    • XSS protection (Next.js built-in)

    Troubleshooting

    Check Railway logs:
    railway logs
    
    Common causes:
    • Database connection failure
    • Migration not run
    • Environment variables missing
    • Application crash on startup
    Verify FRONTEND_URL is set correctly in backend environment variables and matches your frontend domain exactly.
    Check:
    1. Google OAuth redirect URI matches production URL
    2. BASE_URL and FRONTEND_URL are correct
    3. Domain is properly configured with SSL
    4. Corporate domain restriction is set correctly
    Before running migrations:
    # Check database connection
    railway run npx prisma db pull
    
    # Validate schema
    railway run npx prisma validate
    
    # Deploy migrations
    railway run npx prisma migrate deploy
    

    Next Steps

    After deployment:
    • Set up monitoring and alerting
    • Configure automated backups
    • Review Database Management for ongoing maintenance
    • Plan for scaling based on usage metrics
    • Consider implementing CDN for static assets

    Build docs developers (and LLMs) love