How to deploy a MeteorJS application on DigitalOcean

How to deploy a Meteor.js application on DigitalOcean

We may earn an affiliate commission through purchases made from our guides and tutorials.

Let’s get your Meteor app running on DigitalOcean without drama. I’ll show you two clean, proven paths that hold up over time:

  • a single Ubuntu droplet using Meteor Up (Mup) with Docker + Nginx (simple, cheap, fast to redeploy), and
  • DigitalOcean App Platform using a Dockerfile (managed, click-deploy from a repo).

Along the way, I’ll point out the gotchas; Mongo connection strings, ROOT_URL, ports, SSL, restarts—the usual suspects. If you follow one path end-to-end, you’ll have a production deploy you can explain (and fix) at 2 a.m.

Path A: One-droplet deployment with Meteor Up (Docker + Nginx + Let’s Encrypt)

Mup wraps your Meteor build in Docker, sets up Nginx as a reverse proxy, and handles SSL. You push updates with one command, and you don’t have to babysit Node or systemd. It’s still the most hassle-free way to run Meteor on a VM.

1) Prep your droplet and SSH

Create a small Ubuntu LTS droplet (2GB RAM is a realistic floor for Meteor + Docker), add your SSH key, and note the public IP. If you prefer, set up a floating IP and point DNS now; Mup can issue Let’s Encrypt later. (DigitalOcean panel bits aren’t rocket science, so I won’t belabor them.)

2) Add Mup to your project and initialize config

From your Meteor project root on your laptop/workstation:

npm i -D mup
npx mup init

This creates mup.js and settings.json. We’ll edit mup.js so Mup knows your host, your domain(s), and that it should front your app with Nginx and TLS.

3) Get your MongoDB connection string sorted

You have two sane options:

  • DigitalOcean Managed MongoDB (recommended): grab the SRV connection string from the DO dashboard. It looks like mongodb+srv://doadmin:...@db-mongodb-<region>-...mongo.ondigitalocean.com/admin?tls=true&authSource=admin&replicaSet=.... You’ll use this as MONGO_URL.
  • MongoDB Atlas: also fine—format is similar.

Meteor reads DB config from the environment, not settings.json. Don’t try to tuck MONGO_URL into settings—use env vars.

4) Configure mup.js

Open mup.js and fill in the blanks. Example with one droplet, one domain, Let’s Encrypt, and an external Managed MongoDB:

module.exports = {
  servers: {
    one: {
      host: '203.0.113.42',      // your droplet IP
      username: 'root',           // or a sudo user
      // pem: '/path/to/id_rsa'   // or use sshAgent: process.env.SSH_AUTH_SOCK
      sshAgent: process.env.SSH_AUTH_SOCK
    }
  },

  app: {
    name: 'my-meteor-app',
    path: '.',

    docker: {
      image: 'zodern/meteor:root' // robust image that matches Meteor/Node automatically
    },

    servers: { one: {} },

    env: {
      ROOT_URL: 'https://app.example.com',
      PORT: 3000,
      MONGO_URL: 'mongodb+srv://doadmin:REDACTED@db-mongodb-nyc3-XXXX.mongo.ondigitalocean.com/admin?tls=true&authSource=admin&replicaSet=db-mongodb-nyc3',
      // Optionally: MONGO_OPLOG_URL for better reactivity with a replica set
      // MONGO_OPLOG_URL: 'mongodb+srv://.../local?replicaSet=...&authSource=admin&tls=true'
    },

    // if you keep private keys, API keys, etc. in settings.json:
    // (run time settings available to Meteor via Meteor.settings)
    // meteor settings file is read at runtime, not build time
    // set `deployCheckWaitTime` longer if your build is heavy
    deployCheckWaitTime: 120,
  },

  proxy: {
    domains: 'app.example.com',
    ssl: {
      // http challenge for Let's Encrypt
      letsEncryptEmail: 'you@example.com'
    }
  }
};

Notes you’ll thank yourself for later:

  • ROOT_URL must be the final public URL with https. Don’t ship with http:// and wonder why OAuth/WebSocket stuff is cranky.
  • Managed Mongo requires TLS (tls=true) and correct authSource/replicaSet params—copy them verbatim from the DO panel.

5) First-time setup and deploy

npx mup setup
npx mup deploy

setup installs Docker and the proxy; deploy builds your Meteor bundle, ships it to the droplet, and starts the container. If you’ve pointed DNS already, Mup will fetch TLS certs and wire Nginx. If something fails, npx mup logs -f tells you why.

(If you’re curious what Mup does under the hood: Dockerizes the bundle, manages the container lifecycle, and puts Nginx in front. That’s why it’s so repeatable.)

6) Routine updates

Commit, push, then:

npx mup deploy

That’s it. The proxy and certs persist; your app restarts cleanly.

7) Importing or restoring data (Managed Mongo)

When you eventually need to seed or migrate, use the connection string DO gives you with mongodump/mongorestore or mongoimport. Example:

mongorestore --uri "mongodb+srv://doadmin:REDACTED@db-mongodb-nyc3-XXXX.mongo.ondigitalocean.com/admin?authSource=admin&replicaSet=db-mongodb-nyc3&tls=true" /path/to/dump

If your tool requires a CA file, include --tlsCAFile as DigitalOcean’s docs indicate.

Path B: DigitalOcean App Platform with a Dockerfile (repo → build → deploy)

If you like managed rollouts, automatic restarts, logs in the panel, and scaling with a slider, App Platform is fine—as long as you bring a Dockerfile for Meteor so the build matches Meteor’s expectations. DO will happily build your container straight from GitHub and keep versions for rollbacks.

1) Add a production Dockerfile to your repo

Use a multistage Dockerfile that builds the Meteor bundle in one stage and runs Node in a slim runtime stage. The zodern/meteor-docker and meteor/node ecosystem images make sure Node/npm versions line up with your Meteor version.

Here’s a solid, no-nonsense example:

# ---- Build stage ----
FROM meteor/node:20 as build
WORKDIR /app

# Optional: speed up npm by copying only package files first
COPY package*.json ./
RUN npm ci

# Install Meteor CLI inside the image
RUN curl https://install.meteor.com/ | sh
ENV METEOR_ALLOW_SUPERUSER=true

# Copy the rest of the app and build a server bundle
COPY . .
RUN meteor npm ci
RUN meteor build --server-only --directory /build --architecture os.linux.x86_64

# ---- Runtime stage ----
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production

# Copy the built bundle and install server deps
COPY --from=build /build/bundle /app
WORKDIR /app/programs/server
RUN npm ci --omit=dev

# App runs on PORT; App Platform will inject PORT
ENV PORT=8080
WORKDIR /app
EXPOSE 8080

# You MUST provide MONGO_URL and ROOT_URL as App Platform env vars
CMD ["node", "main.js"]

Why this works: Meteor is compiled to a Node bundle; App Platform just needs a container that runs node main.js and respects PORT. You’ll set MONGO_URL and ROOT_URL in the App’s environment settings (dashboard) before the first deploy.

2) Hook the repo to App Platform

Connect your GitHub repo in the DO control panel → Create > Apps → choose your branch. If DO sees a Dockerfile, it will use it. Configure these environment variables:

  • MONGO_URL = your DO Managed Mongo SRV string (with tls=true, authSource=admin, replicaSet=...)
  • ROOT_URL = https://app.example.com (use your real domain)
  • Optional: MONGO_OPLOG_URL for reactive performance on replica sets

App Platform builds the image, deploys, and exposes the app on the given domain. You can also add a “Deploy to DigitalOcean” button to your README for one-click setups later.

Common production details (don’t skip these)

Environment variables that matter: MONGO_URL, ROOT_URL, and the port (PORT). Meteor expects these in the environment in production. Don’t try to cram them into settings.json. And don’t confuse meteor run --production with real production—use an actual bundle or Docker image as shown above.

TLS/SSL: with Mup + proxy, Let’s Encrypt is automatic if DNS points to the droplet. On App Platform, attach your domain and enable the cert in the panel—also automatic.

Files and images: serve user uploads from object storage (Spaces/S3) instead of the container’s filesystem. Containers are immutable; you’ll lose writes on redeploy.

Scaling: on a droplet, bump the size or add a second droplet + load balancer if needed. On App Platform, slide replicas up and consider a larger CPU/RAM tier.

Logs & monitoring:

  • Mup: npx mup logs -f and Docker logs.
  • App Platform: built-in logs + metrics in the DO dashboard.
  • Meteor-specific health: stick a basic /healthz route so load balancers don’t guess.

When you want the old-school “I manage it all” path

You can absolutely run a Meteor bundle under PM2 behind Nginx on a droplet. It still works: build locally, upload the bundle/, npm ci in programs/server, export MONGO_URL and ROOT_URL, then pm2 start. Nginx proxies to the app port. It’s just more moving parts you own, which is why Mup and Docker became the default.

Example—starting a built bundle with PM2:

export MONGO_URL="mongodb+srv://doadmin:REDACTED@db-mongodb-nyc3-XXXX.mongo.ondigitalocean.com/admin?tls=true&authSource=admin&replicaSet=db-mongodb-nyc3"
export ROOT_URL="https://app.example.com"
export PORT=3000
pm2 start /opt/myapp/bundle/main.js --name my-meteor-app
pm2 save

Quick troubleshooting checklist

  • White page or infinite spin: ROOT_URL mismatch or missing. Fix it.
  • Real-time updates flaky: add a proper MONGO_OPLOG_URL against a replica set.
  • “Cannot connect to Mongo” on DO Managed Mongo: you missed tls=true, wrong authSource, or fat-fingered replica set name—copy the string again from the DO panel.
  • Build fails on App Platform: ensure your Dockerfile builds with Meteor’s CLI and that Node versions match your Meteor release (use the Meteor images to avoid guessing).

Was this helpful?

Thanks for your feedback!
Alex is the resident editor and oversees all of the guides published. His past work and experience include Colorlib, Stack Diary, Hostvix, and working with a number of editorial publications. He has been wrangling code and publishing his findings about it since the early 2000s.

Leave a comment

Your email address will not be published. Required fields are marked *